漢字の重心を調べてみた

#暇だったので…

漢字って割とバランス整ってると思いませんか？
（これは重心が関係してるのでは？？？？暇だしちょっと調べてみるか〜）
と思い立ったのが運の尽き。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">暇すぎて常用漢字全部の重心求めた<br>ほとんど真ん中でおもんない <a href="https://t.co/wzoxsxvlfb">pic.twitter.com/wzoxsxvlfb</a></p>&mdash; 15種のヘルシーサラダ (@R01_G03_Y08) <a href="https://twitter.com/R01_G03_Y08/status/1167459221178212352?ref_src=twsrc%5Etfw">August 30, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

予想外に伸びてしまい後に引けなくなったのでちゃんと書くことにしました。

#方針

1. PILで文字入りの画像を作成
2. 2値のnuppy.arrayに変換
3. 文字が中心に来るように調整する
4. 重心を計算して点を打つ


#1. PILで文字入りの画像を作成

```python:gravity.py
from PIL import Image, ImageDraw, ImageFont


def generate_char_img(char, fontname, size=(300, 300)):
    img = Image.new('L', size, 255)
    draw = ImageDraw.Draw(img)
    fontsize = int(size[0] * 0.8)
    font = ImageFont.truetype(fontname, fontsize)

    # adjust character position.
    char_displaysize = font.getsize(char)
    offset = tuple((si - sc) // 2 for si, sc in zip(size, char_displaysize))
    assert all(o >= 0 for o in offset)

    # adjust offset, half value is right size for height axis.
    draw.text((offset[0], offset[1] // 2), char, font=font, fill=0)
    return img
```

PIL.ImageFontでフォントが扱えるみたいです。
ImageFont.truetype.getsizeでフォントサイズを取得してだいたい真ん中に描画します。


#2. 2値のnuppy.arrayに変換

重心を計算するために、
黒(グレースケールで0)→1,その他の色(1〜255)→0
のnumpy.arrayに変換します。
※計算しやすくするためここでは色が0以外のピクセルを無視します。誤差は気にしません。暇つぶしなので。

```python:gravity.py
import numpy as np


def im_to_np(im):
    im_array = np.asarray(im)
    return np.array([[[0 if x else 1] for x in y] for y in im_array]).reshape(im_array.shape)
```

#3. 文字が中心に来るように調整する
文字を真ん中に配置していても、フォントサイズと文字の本当の大きさは違う(上下左右に空白部分が存在する)ので中心に来るように調整する必要があります。

any(list)はlistの中身が一つでもTrueの時Trueなのでx軸,y軸にスライスしたベクトルを代入することで文字の端っこを見つけて、マージンを計算して0行列の該当箇所に代入することで中心に配置しました。

```python:gravity.py
def adjust_array(mat):
    Y, X = mat.shape
    top, left = mat.shape
    bottom = 0
    right = 0
    for i, x in enumerate(mat):
        if any(x):
            top = i if top > i else top
            bottom = i if bottom < i else bottom

    for i, y in enumerate(mat.T):
        if any(y):
            left = i if left > i else left
            right = i if right < i else right

    char_area = mat[top:bottom, left:right]
    height, width = char_area.shape
    margin = ((Y - height) // 2, (X - width) // 2)
    matrix = np.full(mat.shape, 0)
    matrix[margin[0]:margin[0] + height, margin[1]:margin[1] + width] = char_area
    return matrix
```

#4. 重心を計算して点を打つ

重心の計算は

```math
M_{nm}=\Sigma^{画像全体}\left(x座標^n×y座標^m×画素値\right)
``` 
とすると、重心の座標 $G$ は 

```math
G=\left(\frac{M_{10}}{M_{00}}, \frac{M_{01}}{M_{00}}\right)
```
で与えられます。

参考：[画像のモーメントについての備忘録](https://plant-raspberrypi3.hatenablog.com/entry/2018/11/13/185057)


```python:gravity.py
def edit_image(im):
    mat = adjust_array(im_to_np(im))
    x = np.array([np.full(mat.shape[1], i) for i in range(mat.shape[0])])
    y = np.array([range(mat.shape[1]) for i in range(mat.shape[0])])

    M = {}
    for n, m in [(0, 0), (1, 0), (0, 1)]:
        M_nm = np.sum(np.power(x, n) * np.power(y, m) * mat)
        M[str(n) + str(m)] = M_nm

    G = (int(round(M["10"] / M["00"])), int(round(M["01"] / M["00"])))

    im_adjusted = Image.fromarray(np.uint8(210 - mat * 210)).convert("RGB")
    im_G = Image.fromarray(np.uint8(210 - mat * 210)).convert("RGB")

    draw = ImageDraw.Draw(im_G)
    r = im_G.size[0] / 25
    draw.ellipse(((G[1] - r, G[0] - r), (G[1] + r, G[0] + r)), fill=(210, 50, 50))

    return im_G, im_adjusted, G, M["00"]
```

調整したnumpy配列をRGB画像に変換→重心の位置に円を描画しました。

#Characterクラス
使いやすくするためclass定義

```python
import os
import re
import gravity


class Character:
    def __init__(self, char, font, size=(200, 200)):
        self.char = char
        self.font = font
        self.size = size
        self.font_name = re.findall(r'(.*/)?(.+)(\.[a-z]{3})', font)[0][-2]
        self.image_raw = None
        self.image_G = None
        self.G = None
        self.M = 0

    def search_g(self):
        im = gravity.generate_char_img(char=self.char, fontname=self.font, size=self.size)
        edited = gravity.edit_image(im)
        self.image_G, self.image_raw, self.G, self.M = edited

    def save_images(self):
        raw_directly = "output_png/{}".format(self.font_name)
        if not os.path.exists(raw_directly):
            os.makedirs(raw_directly, exist_ok=True)
        self.image_raw.save("{}/{}_{}.png".format(raw_directly, self.font_name, self.char), 'png')

        edit_directly = "output_G/{}".format(self.font_name)
        if not os.path.exists(edit_directly):
            os.makedirs(edit_directly, exist_ok=True)
        self.image_G.save("{}/{}_{}_G.png".format(edit_directly, self.font_name, self.char), 'png')

    def main(self):
        self.search_g()
        self.save_images()
```

#いざ実行

漢字は常用漢字,人名漢字,JIS第1,第2水準漢字から重複を除いたリストを用いました。(6735文字！)
<img width="1438" alt="スクリーンショット 2019-09-01 17.50.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205561/2a14a841-5194-d25c-0377-b7f7afbe5363.png">

フォントはヒラギノ明朝,ヒラギノ角ゴシック,Kleeの3種類です。
(当初は、ヒラギノ明朝,Osaka,KleeだったのですがOsakaは一部の漢字が豆腐化したのでやめました。)

```python:main.py
from character import Character
from tqdm import tqdm
import re
import csv

with open("kanji.txt") as f:
    characters = f.readlines()[0]

font_list = ["/System/Library/Fonts/ヒラギノ明朝 ProN.ttc",
             "/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc",
             "/System/Library/Assets/com_apple_MobileAsset_Font5/1765bae07bcefebc499ababa7ecdef963e4f151e.asset"
             "/AssetData/Klee.ttc"]

if __name__ == '__main__':
    for font in font_list:
        with open("output/{}.csv".format(re.findall(r'(.*/)?(.+)(\.[a-z]{3})', font)[0][-2]), 'w') as f:
            writer = csv.writer(f, lineterminator='\n')
            writer.writerow(("character", "G_x", "G_y", "M", "Euclid"))
            with tqdm(characters) as pbar:
                for i, char in enumerate(pbar):
                    pbar.set_description("[{}:{}]" .format(i, char))
                    c = Character(char=char, font=font, size=(500, 500))
                    c.main()
                    writer.writerow((char, c.G[0], c.G[1], c.M, c.euclid))
```


各columnの内容は以下の通り

| G_x | G_y | M | Euclid |
|:-:|:-:|:-:|:-:|
| 重心のx座標  |  重心のy座標 | $M_{00}$(画像の重さ) | 中心との距離  |


#結果発表

やっと結果発表です、お疲れ様です。

###重心が偏ってる漢字top5

####全漢字

#####ヒラギノ明朝

|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 3303 | 厂 | 176 | 194 | 11919 | 92.800862 |
| 3714 | 广 | 192 | 195 | 12383 | 79.931220 |
| 3284 | 匚 | 179 | 253 | 16545 | 71.063352 |
| 3734 | 廴 | 193 | 285 | 19819 | 66.887966 |
| 3624 | 尸 | 233 | 189 | 16871 | 63.324561 |


#####ヒラギノ角ゴシック
|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 3303 | 厂 | 197 | 183 | 15587 | 85.428333 |
| 3734 | 廴 | 188 | 284 | 21880 | 70.710678 |
| 3624 | 尸 | 236 | 182 | 21906 | 69.426220 |
| 130 | 下 | 260 | 184 | 20691 | 66.753277 |
| 3714 | 广 | 205 | 204 | 15824 | 64.350602 |

#####Klee

|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 3303 | 厂 | 204 | 182 | 13166 | 82.097503 |
| 3714 | 广 | 213 | 200 | 14470 | 62.201286 |
| 3624 | 尸 | 255 | 188 | 20860 | 62.201286 |
| 3734 | 廴 | 197 | 276 | 20071 | 59.033889 |
| 130 | 下 | 258 | 194 | 18921 | 56.568542 |



####常用漢字

#####ヒラギノ明朝

|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 1073 | 仁 | 206 | 253 | 18033 | 44.102154 |
| 1042 | 心 | 243 | 292 | 21276 | 42.579338 |
| 245 | 刈 | 292 | 243 | 30032 | 42.579338 |
| 1499 | 刀 | 290 | 241 | 21582 | 41.000000 |
| 594 | 甲 | 243 | 210 | 27352 | 40.607881 |


#####ヒラギノ角ゴシック

|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 130 | 下 | 260 | 184 | 20691 | 66.753277 |
| 246 | 干 | 248 | 195 | 24481 | 55.036352 |
| 1010 | 上 | 258 | 304 | 21260 | 54.589376 |
| 761 | 止 | 243 | 300 | 26012 | 50.487622 |
| 557 | 戸 | 243 | 200 | 29006 | 50.487622 |

#####Klee

|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 130 | 下 | 258 | 194 | 18921 | 56.568542 |
| 1010 | 上 | 255 | 305 | 19259 | 55.226805 |
| 761 | 止 | 244 | 304 | 24055 | 54.332311 |
| 557 | 戸 | 245 | 197 | 26077 | 53.235327 |
| 594 | 甲 | 248 | 197 | 29236 | 53.037722 |

###重心が偏ってない漢字top5

####全漢字

#####ヒラギノ明朝

|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 3001 | 粕 | 250 | 250 | 36773 | 0.0 |
| 4249 | 樶 | 250 | 250 | 44476 | 0.0 |
| 6279 | 鵝 | 250 | 250 | 45208 | 0.0 |
| 3986 | 攪 | 250 | 250 | 44978 | 0.0 |
| 2606 | 菖 | 250 | 250 | 33132 | 0.0 |

#####ヒラギノ角ゴシック

|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 1420 | 勅 | 250 | 250 | 44996 | 0.0 |
| 1371 | 築 | 250 | 250 | 47119 | 0.0 |
| 3436 | 囓 | 250 | 250 | 55474 | 0.0 |
| 274 | 感 | 250 | 250 | 43700 | 0.0 |
| 6203 | 魍 | 250 | 250 | 55238 | 0.0 |

#####Klee

|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 717 | 材 | 250 | 250 | 31767 | 0.0 |
| 3757 | 彷 | 250 | 250 | 31734 | 0.0 |
| 491 | 慶 | 250 | 250 | 46851 | 0.0 |
| 4997 | 紮 | 250 | 250 | 33211 | 0.0 |
| 6248 | 鰰 | 250 | 250 | 49405 | 0.0 |

####常用漢字

#####ヒラギノ明朝

|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 2128 | 麓 | 250 | 250 | 45407 | 0.0 |
| 694 | 災 | 250 | 250 | 25713 | 0.0 |
| 1571 | 内 | 250 | 251 | 29141 | 1.0 |
| 47 | 因 | 250 | 249 | 35050 | 1.0 |
| 193 | 懐 | 250 | 249 | 42986 | 1.0 |

#####ヒラギノ角ゴシック

|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 1504 | 豆 | 250 | 250 | 36011 | 0.0 |
| 1455 | 提 | 250 | 250 | 45763 | 0.0 |
| 1420 | 勅 | 250 | 250 | 44996 | 0.0 |
| 1371 | 築 | 250 | 250 | 47119 | 0.0 |
| 274 | 感 | 250 | 250 | 43700 | 0.0 |

#####Klee

|  | character | G_x | G_y | M | Euclid |
|--:|:--|--:|--:|--:|--:|
| 491 | 慶 | 250 | 250 | 46851 | 0.0 |
| 159 | 箇 | 250 | 250 | 41478 | 0.0 |
| 1558 | 栃 | 250 | 250 | 37857 | 0.0 |
| 717 | 材 | 250 | 250 | 31767 | 0.0 |
| 816 | 持 | 249 | 250 | 36551 | 1.0 |

#感想

へぇ。って感じですね。
JIS水準漢字とかあまり知らなかったので知れて良かったです。暇は潰せました。では。
[ソースコードと結果(CSV)](https://github.com/ingk-124/FONT)