やること
これまでいろいろな問題を遺伝的アルゴリズムで最適化してきましたが、実は画像の最適化は別格の難しさがあります。
例えば50*50サイズの小さなカラー画像でもピクセルとしては50*50*3=7500次元の情報を持っており、これは、他の問題タイプにおいて100次元そこそこでヒーヒー言っていたのが可愛くなるほどの次元数です。正面から挑むのは危険すぎます。
一応、過去にGAで画像の最適化に挑戦したことがあり、実はその手法で人工知能学会で発表していたります。(本名バレるのでやめて)
これは特殊な例で、モザイク様のノイズの最適化だったため有効でした。無数にある局所解の一つで十分なイメージです。
しかし一般的な画像の最適化は難しいので、色々試した結果をここに記します。コードはあまり洗練していません。
試したこと
ピクセル同士の関係性に着目する
ピクセルを独立に最適化するのは明らかに効率が悪い。人にとって大切なのは色の絶対値ではなく「隣のピクセルとの相対的な色」。そこで「ピクセル同士の関係性」を最適化すればよいではないか?ピクセル同士を鎖で繋いでその鎖を最適化するイメージ。
→良さそうだがさほど次元が減っていない。期待度は低め。
二次元フーリエ変換
画像は二次元フーリエ変換で周波数成分に変換できる。周波数成分は”逆”二次元フーリエ変換で画像に戻せる。そこで「目的の画像が復元されるように周波数成分を最適化」すればよいではないか?
→良さそうだがさほど次元が減っていない。低周波成分をカットしたりしてもやはりさほど減っていない。一応やってみたが解空間が悪魔みたいで一撃で心を打ち砕かれる。一応残骸を貼っておきます。
図形を合成していく
結局、人が絵を描くように図形を合成していくのがよいのではないか?
→まあこれでしょう。ちなみに先人がいます。
人の描き方に習いたいので、大きな図形から塗っていき、最後に小さな図形で細部を整える。多段階のGAで描いてみましょう。
実行環境
WinPython3.6をおすすめしています。
Google Colaboratoryが利用可能です。
vcoptの使い方についてはチュートリアルをご参照ください。
vcoptの仕様については最新の仕様書をご参照ください。本記事執筆時とは仕様が異なる場合があります。
問題とサンプル画像
ここではフェルメールの「真珠の耳飾りの少女」をGAで作成することを目的とします。200*200サイズですが後に50*50にリサイズして使います。
つまり「まっさらなキャンバスに図形を貼り込んでいく。できるだけ耳飾りの少女に近くなるように最適化する」という問題です。面白みはありません。
pip, import
vcoptをインストールします。
pip install vcopt
今回用いるパッケージをインポートします。
import cv2
import numpy as np
import numpy.random as nr
from copy import deepcopy
import matplotlib.pyplot as plt
from vcopt import vcopt
パラメータや初期化など
画像を表示する関数を用意します。多用します。
#表示関数
def show(img):
plt.imshow(img, vmin = 0, vmax = 255)
plt.show()
plt.close()
画像を読み込んで50*50にリサイズして表示します。
#画像サイズ
w_aim = 50
h_aim = 50
#画像読み込みと表示
img = cv2.imread('9-22_0.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (w_aim, h_aim))
show(img)
塗り途中の画像(img_master)と確定した遺伝子(para_master)を初期化します。
#マスター画像
img_master = np.ones((h_aim, w_aim, 3), np.uint8) * 128
show(img_master)
#マスター遺伝子
para_master = np.array([])
print(para_master)
[]
キャンバスはグレーから開始し、確定した遺伝子もありません。
大きい図形から順番に貼っていくので、半径の列を生成します。画像サイズが50の場合、25~0まで徐々に減少する長さ500の列を生成して、その後ろに50個の0を付けます。要するに半径25の図形から順に貼り込んでいき、最後は半径0の円(十字マークになります)で細部を整える、です。
#半径列
w = max(w_aim, h_aim)
rs = np.linspace(0, w/2, w*10)[::-1]
rs_ = np.zeros(w)
rs = np.append(rs, rs_)
rs = rs.astype(int)
print(rs)
[25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23
23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22
22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21
21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19
18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17
17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16
16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15
15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14
14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13
12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11
11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9
9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8
8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7
6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5
5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4
4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
遺伝子設計
以下の図で伝わってください!
確定した遺伝子から画像を生成する関数
遺伝子は徐々に確定してpara_masterに追加されていきます。任意のpara_masterから画像を生成する関数を用意します。
#確定した遺伝子をすべて貼る関数
def make_img(para):
#定義
num = len(para)//5
img_tmp= np.ones((h_aim, w_aim, 3), int) * 128
para_tmp = deepcopy(para)
rs_tmp = deepcopy(rs)
#遺伝子の変形
para_tmp = para_tmp.reshape(-1, 5)
#遺伝子の分離
points = para_tmp[:, :2]
points[:, 0] *= w_aim
points[:, 1] *= h_aim
points = points.astype(int)
#print(points)
colors = para_tmp[:, 2:] * 255
colors = colors.astype(int)
#print(colors)
#貼り付け
for i in range(num):
point = points[i]
color = colors[i]
tmp = deepcopy(img_tmp)
cv2.circle(tmp, (point[0], point[1]), int(rs_tmp[i]), (int(color[0]), int(color[1]), int(color[2])), thickness=-1)
img_tmp = 0.5*tmp + 0.5*img_tmp
img_tmp = img_tmp.astype(int)
return img_tmp
#適当な遺伝子を作成して画像を確認
para = nr.rand(5*3)
para_master = np.append(para_master, para)
img_master = make_img(para_master)
show(img_master)
長さ15の遺伝子によって円が3つ貼られました。円の半径は 25, 24, 24 です。
ある部分遺伝子を貼ってスコアを返す関数
GAで用いる評価関数です。任意の長さの候補遺伝子を受け取り、すでに確定しているマスター画像に貼り、お手本画像との差をスコアとして返します。
関数の仕様上para以外を受け取ることができないので、グローバルからimg_master(マスター画像)、img(お手本画像)、rs(半径列)、step(≒何番目の半径を使うか)なんかを拾ってきます。実行環境によってはバグるかもしれません。
#ある部分遺伝子を貼ってスコアを返す関数
def paste_score(para, visible=False):
#定義
num = len(para)//5
img_tmp = deepcopy(img_master)
para_tmp = deepcopy(para)
rs_tmp = rs[step:step + num]
#遺伝子の変形
para_tmp = para_tmp.reshape(-1, 5)
#遺伝子の分離
points = para_tmp[:, :2]
points[:, 0] *= w_aim
points[:, 1] *= h_aim
points = points.astype(int)
#print(points)
colors = para_tmp[:, 2:] * 255
colors = colors.astype(int)
#print(colors)
#貼り付け
for i in range(num):
point = points[i]
color = colors[i]
tmp = deepcopy(img_tmp)
cv2.circle(tmp, (point[0], point[1]), int(rs_tmp[i]), (int(color[0]), int(color[1]), int(color[2])), thickness=-1)
img_tmp = 0.5*tmp + 0.5*img_tmp
img_tmp = img_tmp.astype(int)
#スコア
diff = (img - img_tmp)**2
diff = np.mean(diff)**0.5
if visible:
show(img_tmp)
return diff
#新しい遺伝子を貼り込み
step = 3
para = nr.rand(5*3)
score = paste_score(para, True)
print(score)
#マスター遺伝子とマスター画像を更新
para_master = np.append(para_master, para)
img_master = make_img(para_master)
#また新しい遺伝子を貼り込み
step = 6
para = nr.rand(5*3)
score = paste_score(para, True)
print(score)
99.49988676710475
94.09123161414493
先ほどのマスター画像に円を3個貼る作業を2回行いました。分かりにくいですが上書きで塗り足されています。次に進む前にimg_masterとpara_masterは初期化します。
多段階GAで最適化
説明がしんどくなってきました。。1ステップでは3個の円を貼る作業を最適化します。これを半径列が尽きるまで繰り返します。
def GA_step(img_master, para_master, step):
#パラメータ範囲
para_range = np.zeros((5*3, 2))
para_range[:, 0] = 0
para_range[:, 1] = 1
#print(para_range)
#GAで最適化
para, score = vcopt().rcGA(para_range, #パラメータ範囲
paste_score, #評価関数
0, #目標値
pool_num=50, #個体数
max_gen=10000, #進化世代数
show_pool_func='bar') #バー出力を使う
#ステップを更新
step += 3
print(step)
#マスター遺伝子を更新
para_master = np.append(para_master, para)
#print(para_master)
#マスター画像を更新
img_master = make_img(para_master)
show(img_master)
return img_master, para_master, step
#ステップの初期化
step = 0
#多段階GA開始
while step < len(rs) - 3:
#1段階進める
img_master, para_master, step = GA_step(img_master, para_master, step)
score = 26.570223935827112
最適化時間は15分程度でした。フェルメールさん本当にごめんなさい。
先駆者の画像でも試してみた
フシギダネ
ペーパーマリオ
キャンバスを黒にするのはフェアじゃないのでグレーのまま実行しました。先駆者さんはどれくらい時間をかけているのでしょうか?ぜひ同じ時間で比較したいですね!
まとめ
そもそも遺伝的アルゴリズムは生物の進化を模倣した最適化アルゴリズムです。今回はさらに人が絵を描くプロセスを模倣しているわけですから、もう人では?という印象です(意味不明)。
パラメータを調整すればもう少しまともな絵が描けそうですが、どうでしょうか。需要があるようならvcoptの関数として正式に実装しようかと考えています。