OpenCV勉強シリーズ
リアルな写真から塗り絵を作成する記事は多いのですが、反対に色を塗るのはあまり見当たらないので勉強がてら色々いじってみました。まあ塗り絵は人間が好きな配色で自由に行うものですが( ´Д`)y━・~~
キャラクターの塗り絵だと権利的に加工や公開NGのところが多いので、問題なさそうなトヨタが公開してる塗り絵を利用します。ディープラーニングは出てきません。
こちらをモデルにします。クラウン!
色々試したパターン
先に塗り絵の結果をご紹介。
1. 均等に配色
2. 車や背景の特徴を考慮して配色
3. ハイブリッド(車の特徴を押さえつつ適度にランダム)
下準備
Pythonのopencv-pythonを使います。PDFファイルはPNGにして書き出しましょう。
作業は大きく3つに分かれます。
- 画像を読み込む
- 輪郭を検出
- 各オブジェクトを塗りつぶす
画像を読み込む
画像の読み込みは単純でCOLOR_BGR2GRAYを指定します。
img_gray = cv2.imread(file_path, cv2.COLOR_BGR2GRAY)
輪郭を検出
意外と難しいところです。輪郭の検出は例によってfindContoursを使いますがポイントはRETR_TREEを使うところです。
contours, hierarchy = cv2.findContours(img_bin, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
以前の記事では将棋の駒の輪郭(外枠)を取得したかったのでRETR_EXTERNALを使っていましたが、塗り絵は人や物のイラストがメインになるので内側の輪郭も必要になります。
下の2つを比べると分かりやすいですがRETR_EXTERNALだとホイールやドアの取っ手が表示されていません。また、右上と右下の文字が消えていて不自然に見えてます。
各オブジェクトを塗りつぶす
いよいよ塗っていきます。具体的にはfindContoursの戻り値 contoursに対して色付けします。
先程の1. 均等に配色ではcontoursにそれぞれ色(RGB)を均等に割り振っています。
colors = [
(250, 250, 250),
(80, 150, 200),
(40, 80, 120),
(227, 132, 9),
(66, 210, 110),
(168, 121, 253)
]
for i in range(len(contours)):
color = colors[i % len(colors)]
img_coloring = cv2.fillPoly(
img_coloring,
[contours[i]],
color,
lineType=cv2.LINE_4,
shift=0
)
img_coloring = cv2.drawContours(img_coloring, [contours[i]], -1, (30, 30, 30), 1)
色がバラバラなことでビルの窓ガラスが良い感じです。
しかし若干気になる所があります。前と後ろのドアで色が違っていたり文字毎に色が違ってます。
そこで次に塗り絵が車のイラストであることを前提に車や背景の色付けすることを考えます。一般的に車は宙に浮いていません。つまりイラストの真ん中から下の方に配置されるわけです。車体は正面だったり真横を向いてるような構図であっても横長になるはずです。加えてイラストとなるような車は野外にある はずで上部に空があります。
上記を踏まえると画面の下の方は地面と仮定して暗く、上部は白や青のような鮮やかな方が自然に見えるはずです。ここまでをざっくりコードで表現してみます。
img_gray = cv2.imread(file_path, cv2.COLOR_BGR2GRAY)
height, width = img_gray.shape[:2]
for i in range(len(contours)):
# 輪郭の左上、右上、左下、右下
min_x, min_y = np.min(contours[i], axis=0)[0]
max_x, max_y = np.max(contours[i], axis=0)[0]
# 輪郭の横、縦幅
contour_width = max_x - min_x
contour_height = max_y - min_y
# 横幅の方が長い判定
long_side = contour_width > contour_height
# 上部15%で横長
top_position15 = height * 0.15 > min_y and max_x - min_x > 100
# 上部30%の位置判定
top_position30 = height * 0.3 < min_y
# 下部30%の位置判定
bottom_position30 = height * 0.7 < min_y
上記のようなパラメータの判定を使うことで輪郭の位置やサイズである程度は統一的なルールを持って色付けすることが可能になります。ちなみに0.3や0.7のように比率で計算してる所は塗り絵のサイズがある程度決まってるなら固定値(ピクセル)で扱っても良いと思います。
↓先程の 2. 車や背景の特徴を考慮して配色
地面、車、空の3色になんとなく”ルールのある塗り絵感”が出てきましたが、もう少し頑張りたいです。1. 均等に配色の無秩序な配色がなくなって味気ない感じです。
これまではcontours(輪郭)の位置やサイズで判断していましたが、次はそれぞれの輪郭同士の位置・サイズの関係性を考慮して配色を行います。やりたいことはメインの車の色をなるべく少なくして、背景はごちゃごちゃした感じにしたいです。
この辺り結構悩みましたが、ざっくりと大きい輪郭が集中してる箇所を重要なオブジェクトと判断します。反対に比較的小さい輪郭が集中してる箇所を塗り絵的にあまり重要ではない(統一感のない配色で良い)と判断します。
車を判定する
とりあえず車のおおよその位置を算出します。
まずcontoursをループで回してwidthを算出して降順でソートします。ここで言うwidthとは輪郭の最も右の位置から最も左を引いた値を指しています。この時、上下左右の端っこにある小さいオブジェクトを除外(filter)しておくと効率が良いです。widthの降順になったところで先頭から10件ほどに配列を切り出します。これでwidthが大きいランキング配列ができました。
ここでひと手間加えて、輪郭の高さ(最も大きい値のyから最も小さいyを引いた値)が最も大きいデータを除外しておきます。理由は車以外にも横幅の大きいオブジェクトがあるかもしれず、車のパーツで最も高い位置にあるのは車の真ん中辺りなので車の外枠を抽出する目的では除外しても問題ないと思われます。
上記通りに進めると要素数9の配列があるはずです。確認のために車の外枠を付けてみましょう。左上、右上、左下、右下、つまりxとyのそれぞれmaxとminを算出します。
drawContoursの固定値で外枠を付けるとこが意外と大変だったのでコード載せます。
car_top = min(x['min_y'] for x in car_contour_rank)
car_bottom = max(x['max_y'] for x in car_contour_rank)
car_left = min(x['min_x'] for x in car_contour_rank)
car_right = max(x['max_x'] for x in car_contour_rank)
# 四隅に外枠をつける
img_coloring = cv2.drawContours(img_coloring, [array([
car_left, car_bottom,
car_right, car_bottom,
]), array([
car_left, car_top,
car_right, car_top,
]), array([
car_left, car_bottom,
car_left, car_top,
]), array([
car_right, car_bottom,
car_right, car_top,
])], -1, (0, 0, 255), 3)
上記の通りに実装するとこんな感じで車体の位置を導き出すことができました。
ここまでくると簡単です。車体の四隅の外、特に上部や両サイドの隅っこをカラフルにして車体を2. 車や背景の特徴を考慮して配色 と同じような配色設定にします。
まだ右下と右上の文字色がバラバラになってますが、これも有りな気がしてきたのでこれで完成とします。
最後に、他の車も↑と同じルールで塗り絵してみました。
感想まとめ
追究していくとディープラーニングが最強って結論になりますが、Text DetectionやfindContoursを工夫するともっとクオリティが上がると思います。
単なる塗り絵の自動化にこれまで需要を感じたことはないですが、漫画とかのベタ塗りとかに応用できないかな?とか思いました。ジョジョのアニメはシーンによってキャラの配色が変わったりするんですよね。
色はAdobe ColorやMac標準のDigital Color Meterといったツールが便利でおすすめです。
https://color.adobe.com/ja/create/color-wheel
コード全部載せようと思ったのですが長くてごちゃごちゃしたのでやめました(´-`).。oO