DAZでレンダリングした画像を python でクラスタリングしたりレタッチしたりしてアニメ風の厚塗りにしてみたい。
HSV系に変換して明るさを離散的にして色々と編集する試行のメモです。
ちなみに python 初心者なんで検証はしてるけどコード自体は怪しいです。
実験用DAZ画像の準備
なにはともあれDAZでレンダリング。
SAKURA8をベースに適当に髪とシャツを着せてIray Ghost Lightの Wall Light で正面から照らしてレンダリングします。このモデルは目がパーツとして分かれてるので便利です。
Iray じゃなくて 3Delight のほうがのっぺりするかもしれませんが、アップデートしてから 3Delight 動かないんですよね。
pythonでHSV変換して明度を離散化
アニメ塗りって色数少なくて明度が連続してないように見える。
とりあえず明るさを複数段階に分けてみます。
引数でファイル名を受け取って output/[ファイル名]
フォルダを作成。
α値でマスクを作ってBGR(RGB)系からHSV系に変換します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import os import cv2 import numpy as np import argparse def main(): parser = argparse.ArgumentParser() parser.add_argument("file",type=str) args = parser.parse_args() global output_dir output_dir="output/"+os.path.basename(args.file)+"/" os.makedirs(output_dir,exist_ok=True) img = cv2.imread(args.file,-1) mask = img[:,:,3] hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # BGR => HSV h,s,v = cv2.split(hsv) |
輝度値Nがリストの中のどの数値に一番近いかを返す関数を作る。
次にforで新しい輝度値を入れていく。
ところで行列の定義方法こんなんでいいんだろうか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def getN(n,list): return list[np.abs(np.asarray(list) - n).argmin()] ... median_v = np.median(v[v!=0]) zero = np.zeros(v.shape,dtype="uint8") v0,v1,v2,v3 = zero.copy(),zero.copy(),zero.copy(),zero.copy() for i,r_v in enumerate(v): # y for j,r_c in enumerate(r_v): # x v0[i][j]=median_v v1[i][j]=getN(r_c,[0,128,255]) v2[i][j]=getN(r_c,[0,64,128,192,255]) v3[i][j]=getN(r_c,[0,32,64,96,128,160,192,224,255]) |
保存はこんな感じでHSV=>BGR変換してアルファ値を戻してます。
1 2 3 4 5 6 7 8 9 10 11 12 |
def hsv2rgb(h,s,v): return cv2.cvtColor(cv2.merge((h,s,v)), cv2.COLOR_HSV2BGR) def hsv2rgba(h,s,v,mask): return cv2.merge((hsv2rgb(h,s,v),mask)) ... cv2.imwrite(output_dir+"r0.png", hsv2rgba(h,s,v0,mask)) cv2.imwrite(output_dir+"r1.png", hsv2rgba(h,s,v1,mask)) cv2.imwrite(output_dir+"r2.png", hsv2rgba(h,s,v2,mask)) cv2.imwrite(output_dir+"r3.png", hsv2rgba(h,s,v3,mask)) |
昔のゲームみたいな色合いになりました。
median
はさすがに黒い部分が変な色になるのは困るので v0
の調整。
標準偏差とか考えましたが意味なさそうだったんで適当なマジックナンバー。
1 2 3 4 5 6 |
if r_c < 120:#median_v-2*sig: v0[i][j]=0 elif r_c > median_v+120: v0[i][j]=255 else: v0[i][j]=median_v |
これらを適当に合成してみます。
全体的にノイズっぽいのでぼかしてオーバーレイで重ね、元画像の輝度値にする。
比較するとこんな感じ。ちょっと平面感が出た。
HSVのヒストグラムをプロット
さすがに数値の割り振りが適当すぎたので、ちょっとヒストグラムを確認します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
from matplotlib import pyplot as plt def plotHist(h,s,v,mask,outdir="output"): fig = plt.figure(figsize=[9,6]) ax1 = fig.add_subplot(1, 1, 1) ax1.hist(h[mask!=0].ravel(),256,[0,256],color='red', alpha=0.5) ax1.hist(s[mask!=0].ravel(),256,[0,256],color='green', alpha=0.5) ax1.hist(v[mask!=0].ravel(),256,[0,256],color='blue', alpha=0.5) ax1.set_ylim(0,10000) fig.tight_layout() plt.savefig(outdir+"__hist1.png") def plotHist2(h,s,v,mask,outdir="output"): fig = plt.figure(figsize=[6,9]) ax1 = fig.add_subplot(3, 1, 1) ax1.set_title("Hue") ax2 = fig.add_subplot(3, 1, 2) ax2.set_title("Saturation") ax3 = fig.add_subplot(3, 1, 3) ax3.set_title("Value") #ax1.set_ylim(0,10000) #ax2.set_ylim(0,10000) #ax3.set_ylim(0,10000) ax1.hist(h[mask!=0].ravel(),256,[0,256]) ax2.hist(s[mask!=0].ravel(),256,[0,256]) ax3.hist(v[mask!=0].ravel(),256,[0,256]) fig.tight_layout() plt.savefig(outdir+"__hist2.png") |
今回は画像に白い部分が多かったので彩度0や輝度255が圧倒的に多い。
無彩色は Hue が 0 になるのでそこも多い。
これを k をいくつにするかの参考にする。
k-means法でクラスタリング
均一に分けすぎていたのでクラスタリングしてみます。
Hueを5つに、Vlarueを10段階にしてみる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
criteria = cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 10, 1.0 _, _, centers_h = cv2.kmeans( h[mask!=0].ravel().astype(np.float32), 5, None, criteria, attempts=10, flags=cv2.KMEANS_RANDOM_CENTERS ) _, _, centers_v = cv2.kmeans( v[mask!=0].ravel().astype(np.float32), 10, None, criteria, attempts=10, flags=cv2.KMEANS_RANDOM_CENTERS ) clst_h = centers_h.ravel().astype(np.uint8).tolist() clst_v = centers_v.ravel().astype(np.uint8).tolist() hx,vx = zero.copy(),zero.copy() for i,r_v in enumerate(v): # y for j,r_c in enumerate(r_v): # x hx[i][j]=getN(h[i][j],clst_h) vx[i][j]=getN(r_c,clst_v) cv2.imwrite(output_dir+"rx4.png", hsv2rgba(hx,s,vx,mask)) |
なんか思ったよりノイジー。
これをベースにしつつ、輝度レイヤーをやめてハイパスやコピーで線を出す。
元の色合いを残しつつ立体感を消せてきたような気がする。
寄り道 RBGのクラスタリング
ここでふと疑問に思った。
単純にRGBの色セットをクラスタリングするとどうなるんだろう。
試行回数が足りなさそうだったので増やしましたが結構時間がかかります。
1 2 3 4 5 6 7 8 9 10 11 |
criteria = cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 50, 1.0 for i in [2,4,8,16,32,64,128,256]: _, labels, centers_rgb = cv2.kmeans( img.reshape((-1,3)).astype(np.float32), i, None, criteria, attempts=20, flags=cv2.KMEANS_RANDOM_CENTERS ) dst = centers_rgb[labels].reshape(img.shape) print(centers_rgb) print() cv2.imwrite(output_dir+f'___dest{i:03}.png',cv2.merge((dst.astype("uint8"),mask))) return |
ファンシーインデックスというのを見て centers_rgb[labels]
とすれば楽だとこうしたけど、img[mask!=0]
で mask
適応ができないので for
文まわした方が良かった。
対象によってはありかもしれないけどよっぽど今回みたいに色と光量が複雑でない時しか使えなさそう。多分HSVに分けた方が意味がありそう。
これはこれでどこかで使うかもしれないので頭に残しておく。
寄り道 HSVのクラスタリング
じゃあHSVをそのままクラスタリングするとどうなるか。
思ったよりいい感じの結果が出た。
Hueが0と255でつながっているのでおかしくなるかと思いましたが、cv2.kmeans
の 0 付近は青色のため今回の画像では問題なかったのかもしれない。
この辺からk を自動計算する pyclustering.cluster
の xmeans
や gmeans
も使ってみましたが、kが異常に増えて使い物にならなかったので止めました。正しい分布があるわけじゃないし仕方ない。
目標を考える
最終的にどんな感じの色合いを目指すかを手動で作ってみる。
前にやった対象ごとにレンダリングする方法を使って、各パーツを単色塗りする。
目だけは複雑なのでそのまま。
こんな感じの絵が自動で作れると嬉しい。
ハイパス・コピーで線を足しつつ、オーバーレイ・輝度で陰影を多少追加する。
比較するとやっぱり手動がそれっぽい。
多分次回に続きます。
所感
久しぶりというか気付いたら12月になってた怖い。
これまでも画像生成AIとか新規技術は触ってみてたんだけど色々な事情で書きにくかったです。
今回はじめてちゃんと python 書いた気がするけど、初期の js を思い出す自由さ。
クラスタリング探してるときに最初に python のを見つけたからそのまま書いてたけど、まずインデントで制御するのが面倒くさい。
行列の扱いがフワフワしすぎてコード読んだ時の動きがわかりにくい。
あとできれば型も欲しい。
ライブラリが多くてぱっと動かすときに便利だけど、それ以外がしんどい。
そのうち慣れるんだろうか。