Python
Python3

月で絵を描きたいだと?それならpythonに任せなさい🌝

月で絵を描く

 最近、月の絵文字で絵を描くツイートがバズっていました。

月の絵文字とは以下の8種類です。

🌑
🌒
🌓
🌔
🌘
🌗
🌖
🌕

 これらの絵文字の濃淡を利用することで何かのロゴとか、オルガさんの「止まるんじゃねぇぞ...」とかを描くというわけです。

 これは自動化できる。。。

 そう思った私は早速emacsとterminalを立ち上げ、コーディングを開始しました。思っていたより完成は早く、朝の10時にコーディングを開始しお昼12時半には完成していたと思います。それでは手法とその結果を見ていきましょう。

手法

流れ
1. 画像を読み込む
2. 画像をグレースケールにする
3. 画質を落とす
4. 前処理
5. 画像の一部と月行列でアダマール積を取る(詳細は後で)
6. 5の最大値を取る月行列に対応する月を採用する
7. 5と6を画像の隅々にわたるまで繰り返す

1.画像を読み込む

※pythonのバージョンは3.6.4です。

from PIL import Image

img = Image.open(img_file)

たったこれだけ。

2.画像をグレースケールにする

img = img.convert('L')

たったこれだけ。
python最高。

3.画質を落とす

    width = 300
    height = int(width*(img.height/img.width))
    height -= height%4

    img = img.resize((width, height))

さて、少し煩雑なコードになってしまいました。

正確に説明することが難しいので、やりたいことがわかってくれればそれでいいです。

やりたいことは比をできるだけ保ちながら、縦と横のピクセル数を4の倍数にしたいということ。理由は、この後定義する月行列にあります。

◆月行列

 当然のことながら、月を何らかの数字に置き換えないとコンピュータは演算することができません。そこで私は次のような月行列を定義しました。

行列Aを4x4の正方行列とし、

texclip20180614162253.png

texclip20180614162223.png

明るければ1、暗ければ-1とする。

例えば、

🌒に対応する月行列は、
texclip20180614162914.png

🌗に対応する月行列は、
texclip20180614163002.png

みたいな感じ。

これを事前に8種類分用意しておく。

    tsuki_0 = np.matrix([[-1, -1, -1, -1],
                        [-1, -1, -1, -1],
                        [-1, -1, -1, -1],
                        [-1, -1, -1, -1]])

    tsuki_1 = np.matrix([[-1, -1, -1, 1],
                        [-1, -1, -1, 1],
                        [-1, -1, -1, 1],
                        [-1, -1, -1, 1]])

    tsuki_2 = np.matrix([[-1, -1, 1, 1],
                        [-1, -1, 1, 1],
                        [-1, -1, 1, 1],
                        [-1, -1, 1, 1]])

    tsuki_3 = np.matrix([[-1, 1, 1, 1],
                        [-1, 1, 1, 1],
                        [-1, 1, 1, 1],
                        [-1, 1, 1, 1]])

    tsuki_4 = np.matrix([[1, -1, -1, -1],
                        [1, -1, -1, -1],
                        [1, -1, -1, -1],
                        [1, -1, -1, -1]])

    tsuki_5 = np.matrix([[1, 1, -1, -1],
                        [1, 1, -1, -1],
                        [1, 1, -1, -1],
                        [1, 1, -1, -1]])

    tsuki_6 = np.matrix([[1, 1, 1, -1],
                        [1, 1, 1, -1],
                        [1, 1, 1, -1],
                        [1, 1, 1, -1]])

    tsuki_7 = np.matrix([[1, 1, 1, 1],
                        [1, 1, 1, 1],
                        [1, 1, 1, 1],
                        [1, 1, 1, 1]])

これら月行列を用いて画像から月描画を行います。なので、画質を4の倍数に落とした方が後々得するのです。

(説明適当ですみません)

そして、月行列のindexを上から0~7としました。index→絵文字とする関数も作っておきます。

def index2tsuki(index):
    if index==0:
        return emoji.emojize(':new_moon:', use_aliases=True)
    if index==1:
        return emoji.emojize(':waxing_crescent_moon:', use_aliases=True)
    if index==2:
        return emoji.emojize(':first_quarter_moon:', use_aliases=True)
    if index==3:
        return emoji.emojize(':waxing_gibbous_moon:', use_aliases=True)
    if index==4:
        return emoji.emojize(':waning_crescent_moon:', use_aliases=True)
    if index==5:
        return emoji.emojize(':last_quarter_moon:', use_aliases=True)
    if index==6:
        return emoji.emojize(':waning_gibbous_moon:', use_aliases=True)
    else:
        return emoji.emojize(':full_moon:', use_aliases=True)

pythonでは絵文字を扱う時にはemojiというライブラリを使用するのが手っ取り早いかと思います。pipなどお手元のパッケージインストーラーでインストールしましょう。

話を戻します。

4.前処理

    img = np.matrix(img)
    img = (img/128.) - 1.

これに関してもやりたいことだけ理解してください。
やりたいことは画像のピクセルに格納されている数値を0~255から-1~1に変換したいということです。あんまり脳みそ使わずに実験的に書いたので、細かいことは放っておいて下さい。。。

さて、ここまでで月描画における準備が完了しました。これらのコードを用いて月描画を行います。上までのコードを全て、utils.pyというファイルに関数郡としてまとめておきます。

# -*- coding: utf-8 -*- #

import emoji
import numpy as np
from PIL import Image

def load_tsuki_matrixs():
    """
    月行列を返す関数

    Args:
        無し
    Return:
        月行列8個, (numpyの行列のリスト)
    """
    tsuki_0 = np.matrix([[-1, -1, -1, -1],
                        [-1, -1, -1, -1],
                        [-1, -1, -1, -1],
                        [-1, -1, -1, -1]])

    tsuki_1 = np.matrix([[-1, -1, -1, 1],
                        [-1, -1, -1, 1],
                        [-1, -1, -1, 1],
                        [-1, -1, -1, 1]])

    tsuki_2 = np.matrix([[-1, -1, 1, 1],
                        [-1, -1, 1, 1],
                        [-1, -1, 1, 1],
                        [-1, -1, 1, 1]])

    tsuki_3 = np.matrix([[-1, 1, 1, 1],
                        [-1, 1, 1, 1],
                        [-1, 1, 1, 1],
                        [-1, 1, 1, 1]])

    tsuki_4 = np.matrix([[1, -1, -1, -1],
                        [1, -1, -1, -1],
                        [1, -1, -1, -1],
                        [1, -1, -1, -1]])

    tsuki_5 = np.matrix([[1, 1, -1, -1],
                        [1, 1, -1, -1],
                        [1, 1, -1, -1],
                        [1, 1, -1, -1]])

    tsuki_6 = np.matrix([[1, 1, 1, -1],
                        [1, 1, 1, -1],
                        [1, 1, 1, -1],
                        [1, 1, 1, -1]])

    tsuki_7 = np.matrix([[1, 1, 1, 1],
                        [1, 1, 1, 1],
                        [1, 1, 1, 1],
                        [1, 1, 1, 1]])

    return [tsuki_0, tsuki_1, tsuki_2, tsuki_3,
            tsuki_4, tsuki_5, tsuki_6, tsuki_7]



def index2tsuki(index):
    """
    indexから絵文字対応する絵文字を返す関数
    Args:
        index
    Return:
        絵文字
    """
    if index==0:
        return emoji.emojize(':new_moon:', use_aliases=True)
    if index==1:
        return emoji.emojize(':waxing_crescent_moon:', use_aliases=True)
    if index==2:
        return emoji.emojize(':first_quarter_moon:', use_aliases=True)
    if index==3:
        return emoji.emojize(':waxing_gibbous_moon:', use_aliases=True)
    if index==4:
        return emoji.emojize(':waning_crescent_moon:', use_aliases=True)
    if index==5:
        return emoji.emojize(':last_quarter_moon:', use_aliases=True)
    if index==6:
        return emoji.emojize(':waning_gibbous_moon:', use_aliases=True)
    else:
        return emoji.emojize(':full_moon:', use_aliases=True)


def preprocess(path, col):
    """
    画像の前処理。グレスケール、画質下げ、その他前処理。
    Args:
        path: 画像のパス
        col: 月の列の数
    Return:
        img: 前処理が完了したnumpyの行列
    """
    img = Image.open(path)
    img = img.convert('L')

    width = col*4
    height = int(width*(img.height/img.width))
    height -= height%4

    img = img.resize((width, height))
    img = np.matrix(img)
    img = (img/128.) - 1.

    return img

この関数群をmain.pyから呼び出す形式をとります。

5.画像の一部と月行列でアダマール積を取る

ここが今回の月描画の肝です。

アダマール積とは、下のような要素ごとの掛け算のことです。(Wikiより)
スクリーンショット 2018-06-14 16.44.15.png

if __name__ == "__main__":
    tsuki_matrixs = load_tsuki_matrix()
    img = preprocess(IMAGE_URL)

先ほど定義したutils.pyの中の関数を用いてtsuki_matrix(8つの行列)とimg(1つの行列)を用意します。ここで重要なことは、どの行列も-1~1の範囲で、明るければ1に近く、暗ければ-1に近いという性質を持つ行列であるということです。つまりアダマール積と取り、その合計を取ると、互いの類似度をうまく測ることができます。tsuki_matrixは4x4の行列なので、画像内の4x4の部分のみと類似度を測り、最も類似している月を採用します。それを画像内全ての位置で繰り返せばいいのです。

for n, tk in enumerate(tsuki_matrixs):
    hadamard = np.multiply(img[row:row+4, col:col+4], tk)

6. 5の最大値をとる月行列に対応する月を採用する

max = -10000
max_tk = 0

for n, tk in enumerate(tsuki_matrixs):
    hadamard = np.multiply(img[row:row+4, col:col+4], tk)

    if max < hadamard.sum():
        max_index = n
        max = hadamard.sum()

tsuki_list.append(index2tsuki(max_index))

これで、画像内の4x4ピクセル部分の月が決定します。あとはこれを画像内全体で繰り返せばいいだけです。

7. 5と6を画像の隅々にわたるまで繰り返す

# -*- coding: utf-8 -*- #

import sys
import numpy as np
from PIL import Image

from utils import load_tsuki_matrix, preprocess, index2tsuki

IMAGE_URL = "./images/画像名"

if __name__ == "__main__":
    tsuki_matrixs = load_tsuki_matrix()
    img = preprocess(IMAGE_URL)

    tsuki_list = []

    for i in range(int(np.shape(img)[0]/4)):
        for j in range(int(np.shape(img)[1]/4)):
            row = 4*i
            col = 4*j
            max = -10000
            max_tk = 0

            for n, tk in enumerate(tsuki_matrixs):
                hadamard = np.multiply(img[row:row+4, col:col+4], tk)

                if max < hadamard.sum():
                    max_index = n
                    max = hadamard.sum()

            tsuki_list.append(index2tsuki(max_index))
        tsuki_list.append('\n')



    for i in tsuki_list:
        sys.stdout.write(i)

これで全てのコーティングは終了です。あとは、ソースコード内の画像名の部分をお好みの画像名に変えてあげることで、python君がせっせこせっせこ月描画を始めてくれます。皆さんも是非試してみてください。

ソースコードはこちら

結果

オルガさん
スクリーンショット 2018-06-14 15.49.23.png

安倍さん
abesan.png


スクリーンショット 2018-06-14 16.58.48.png

なかなかの描画性能じゃないでしょうか。

ちなみに僕のツイートは十数リツイートとそんなにバズりませんでした。(他のバズってる月ツイートよりめちゃくちゃ高画質やのに!!!)

また、これを用いて月クイズなんてものを作ってみました。よかったら解いてみて下さい。

最後まで読んでいただきありがとうございました。🌚

月クイズ

※一応難易度順

第一問

スクリーンショット 2018-06-14 17.02.46.png

第二問

スクリーンショット 2018-06-14 17.11.34.png

第三問

スクリーンショット 2018-06-14 17.12.46.png

第四問

スクリーンショット 2018-06-14 17.08.45.png

第五問

スクリーンショット 2018-06-14 17.13.34.png

自己紹介

冒頭に書くと邪魔になるので最後にひっそりと自己紹介させてください。

名前 綿岡晃輝
職業 大学4年生
分野 機械学習, 深層学習, 音声処理
Twitter @Wataoka_Koki