LoginSignup
12
4

More than 3 years have passed since last update.

漢字の重心を調べてみた

Last updated at Posted at 2019-09-03

暇だったので…

漢字って割とバランス整ってると思いませんか?
(これは重心が関係してるのでは????暇だしちょっと調べてみるか〜)
と思い立ったのが運の尽き。

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

方針

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

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

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以外のピクセルを無視します。誤差は気にしません。暇つぶしなので。

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行列の該当箇所に代入することで中心に配置しました。

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. 重心を計算して点を打つ

重心の計算は

M_{nm}=\Sigma^{画像全体}\left(x座標^n×y座標^m×画素値\right)

とすると、重心の座標 $G$ は

G=\left(\frac{M_{10}}{M_{00}}, \frac{M_{01}}{M_{00}}\right)

で与えられます。

参考:画像のモーメントについての備忘録

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定義

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文字!)
スクリーンショット 2019-09-01 17.50.36.png

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

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)

12
4
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
12
4