なぜレガシーな方法を使うのか?
Stable Diffisionはとても便利なツールですが、一つだけ大きなデメリットがあります。
それは持ち込み画の同一性を維持できないことです。AIによる創造的な機能を抜きにして単純に機械処理したい場合もあるかもしれません。以下はそういう時のためのTIPS集になります。
レイヤー分け?
Windowsの場合はlayerdividerというアプリもしくはとても便利な拡張ツールが開発されましたがMacの場合は使えそうもないのでどうやったら良いのかをただいま調査中です。
線画抽出処理
用意する画像はjpg形式でもpng形式でも構いません。拡張子を必要に応じて変更してご利用ください。
import cv2
import numpy as np
image_path = "/Users/(アカウント名)/Documents/test.png"
img = cv2.imread(image_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.merge((gray, gray, gray), img)
kernel = np.ones((4,4),np.uint8)
dilation = cv2.dilate(img,kernel,iterations = 1)
diff = cv2.subtract(dilation, img)
negaposi = 255 - diff
cv2.imwrite('/Users/(アカウント名)/Documents/test_out.png', negaposi)
一度色塗りした宇宙戦艦ヤマトの森雪の自作画像に適用した結果が以下になりました。
塗り層抽出にあたって
まずは画像における代表的な色をサンプリングしそれに近い色層の抽出というやり方をしてみようと思います。
もちろんここでは機械的な作業として行うためこれは人物だとかこれはその中の顔とか手とか背景だとかそういう識別はしないものとしして扱います。単純に同じような色相の機械的なレイヤー分けをします。
代表色リスト抽出
まずStable Diffusionの環境にはIPythonが入っていないかったので先にインストールしておきます。
また場合によってはjupyter notebookを使った方が便利なのでここで一緒にインストールしてしまいます。
%pip install IPython
%conda install jupyter notebook
#パスを調べる。
%which jupyter
#起動
%jupyter notebook
以下の記事を参考にして試してみました。
この方法で色分けすると自分で決めた特定の数で色分けすることになります。以下は5色で分けた場合の例になります。
import cv2
import numpy as np
from IPython.display import Image, display
from matplotlib import pyplot as plt
def imshow(img):
"""ndarray 配列をインラインで Notebook 上に表示する。
"""
ret, encoded = cv2.imencode(".jpg", img)
display(Image(encoded))
# 画像を読み込む。
img = cv2.imread("/Users/(アカウント名)/Documents/test.png")
# 画像で使用されている色一覧。(W * H, 3) の numpy 配列。
colors = img.reshape(-1, 3).astype(np.float32)
# クラスタ数
K = 5
# 最大反復回数: 10、移動量の閾値: 1.0
criteria = cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 10, 1.0
ret, labels, centers = cv2.kmeans(
colors, K, None, criteria, attempts=10, flags=cv2.KMEANS_RANDOM_CENTERS
)
print(f"ret: {ret:.2f}, label: {labels.shape}, center: {centers.shape}")
labels = labels.squeeze(axis=1) # (N, 1) -> (N,)
centers = centers.astype(np.uint8) # float32 -> uint8
# 各クラスタに属するサンプル数を計算する。
_, counts = np.unique(labels, axis=0, return_counts=True)
# 可視化する。
fig, [ax1, ax2] = plt.subplots(1, 2, figsize=(10, 3))
fig.subplots_adjust(wspace=0.5)
# matplotlib の引数の仕様上、[0, 1] にして、(R, G, B) の順番にする。
bar_color = centers[:, ::-1] / 255
bar_text = list(map(str, centers))
# 画像を表示する。
ax1.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
ax1.set_axis_off()
# ヒストグラムを表示する。
ax2.barh(np.arange(K), counts, color=bar_color, tick_label=bar_text)
plt.show()
# 各画素を k平均法の結果に置き換える。
dst = centers[labels].reshape(img.shape)
cv2.imwrite('/Users/(アカウント名)/Documents/test_out.png', dst)
ただし、以下レイヤー分け作業していて気がついたのですが、上の表は座標と色が対応していないことも分かりました。表そのものは分かりやすくて良いものだったのですが、修正して改めて作り直す必要があることが分かりました。
代表色で置き直すと以下のようになりました。
特定色層抽出
次に上で分けられた色層でレイヤー分けができるか試してみたいと思います。
上の色分け程度だと以下のようなレイヤー分けしかできないことも分かりました。
ある程度十分な数の色分けをする必要がありそうです。
また同時にレイヤー同士の差分みたいなロジックで切り取ることも場合によっては有効かもしれません。
もしくはお婆さんにも娘にも見えるような騙し絵みたいな紛らわしい絵でない限りはAI認識による分類によってマスクを作りレイヤー分けする方法も考えられるかもしれません。
これを行うにあたってscipyをインストールしておきます。
%pip install scipy
import cv2
import numpy as np
from scipy.spatial import distance
def extract_color_layer(image, target_color, threshold):
# 画像を読み込む
img = cv2.imread(image)
# BGRをRGBに変換する
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 目標色との距離を計算する
dist = distance.cdist(img_rgb.reshape(-1, 3), [target_color])
# 距離が閾値以下のインデックスを取得する
indices = np.where(dist <= threshold)
# マスクを作成する
mask = np.zeros((img_rgb.shape[0] * img_rgb.shape[1],), dtype=np.uint8)
mask[indices[0]] = 255
mask_2d = mask.reshape((img_rgb.shape[0], img_rgb.shape[1]))
# 元の画像とマスクを重ね合わせて特定の色のレイヤーを取得する
color_layer = cv2.bitwise_and(img_rgb, img_rgb, mask=mask_2d)
return color_layer
# 画像ファイルと目標色を指定する
image_path = 'test.png(画像ファイルまでのパス)'
target_color = np.array([176, 108, 53]) # RGB色を指定する
# 閾値を設定する(距離がこの値以下の色が同じ色とみなされます)
threshold = 164
# 特定の色のレイヤーを取得する
result = extract_color_layer(image_path, target_color, threshold)
# 結果を表示する
cv2.imshow('Color Layer', cv2.cvtColor(result, cv2.COLOR_RGB2BGR))
# 0キーで終了
cv2.waitKey(0)
cv2.destroyAllWindows()
髪と顔の影((152, 207, 213)/閾値100)
肌((230, 237, 248)/閾値35)
服((176, 108, 53)/閾値164)
服の黄色はレイヤー分けが難しいことが分かりました。
服のアクセント部分((99, 109, 113)/閾値40)
宇宙背景((1, 237, 252)/閾値165)
PSDファイル化に向けて?
次にレイヤー分けしたものを黒色部分を透明化したりPSD化する方法を考えてみます。
まずPSD化するライブラリをインストールします。
% pip install pytoshop
(続く)