第8回 トゥーン化

本題に入る前に、必ず 連絡 の動画を見てください。
Google Colabへのリンク

1. リサイズと平滑化

動画の解説を参照

今回は実写の画像を元にして、色数が少なく輪郭の線がある漫画のような画像を作成します。
そのために、第5回に紹介した「メディアンフィルタ」を使って細部の構造を潰します。
import cv2
import numpy as np
from google.colab import files

file = files.upload()
filename = next(iter(file))
img = cv2.imread(filename)
h, w = img.shape[:2]

new_w = 800
new_h = int(h * (new_w / w))
img1 = cv2.resize(img, (new_w, new_h))
img1 = cv2.medianBlur(img1, 9)

cv2.imwrite('ex1.png', img1)
元画像の選び方についての注意
実写から漫画的な画像を作るのが目的なので、元画像は実写 (風) のものにしてください。実写でも極端に色数が少ないものはNGです。
また、輪郭の線を取り出すために、背景に対して人物や建物などのオブジェクトの境界が明確なものにしてください。
例えばこのような画像です (出力画像もこのページに掲載されているので、課題の元画像としてこの画像は使わないでください)
参考 : haconana (AI生成された画像の無料DLサイト)

課題1

上記の太枠の説明の条件を満たす元画像を元にして、横幅が800ピクセルでぼかしの入った画像 ex1.pngを出力してください。

2. 枠画像作成

動画の解説を参照

第7回で紹介した Canny法を使えば、画像から太さ1の輪郭線を取り出せます。
ただし、そのままでは漫画的な輪郭線としては細すぎて使えません。

OpenCVのdilate関数を使えば、白い線を太くできます。
以下のコードの3行目は太さを5にするものです。
img2_1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
img2_1 = cv2.Canny(img2_1, ?, ?)
img2_5 = cv2.dilate(img2_1, np.ones((5, 5), np.uint8), iterations=1)
cv2.imwrite('ex2_1.png', img2_1)
cv2.imwrite('ex2_5.png', img2_5)

課題2

課題1で作成した画像を元にして、輪郭抽出した画像 ex2_1.png, その画像の線の太さを5にした画像 ex2_5.png を出力してください。
※ 輪郭がなるべく途切れず、余計な線ができないように Canny関数の引数を調整してください。

3. 減色

動画の解説を参照

一般的なカラー画像は、赤・緑・青の成分の明るさが0~255の256段階で、\(256^3=(2^8)^3=2^{24}=16,777,216\) 通りの色があります。
これを赤・緑・青それぞれ2段階 (0か255)にすると以下の8色になります。
0 0 0
255 0 0
0 255 0
0 0 255
255 255 0
255 0 255 マゼンタ
0 255 255 シアン
255 255 255
元のそれぞれの色成分 (0~255) を128で割った値の整数部分は、元の値が0~127なら 0, 128~255なら 1 になります。
それに255をかければ、「128より暗いところは0, そうでなければ255」が得られます。
それぞれの色成分について二値化を行うようなものです。
img3_2 = np.uint8(img1 / 128) * 255

同様に、それぞれの色成分を4段階にすると \(4^3=(2^2)^3=2^{6}=64\) 色になります。
それぞれの色成分は、0~255の値を64で割った整数部分 0~3 に 255/3 をかけることで得られます。
img3_4 = np.uint8(img1 / 64) * np.uint8(255 / 3)
さきほどの画像と比べると「使える絵の具」が増えるため、元の画像の色に近くなります。
img3_2 = np.uint8(img1 / 128) * 255
img3_4 = np.uint8(img1 / 64) * np.uint8(255 / 3)
cv2.imwrite('ex3_2.png', img3_2)
cv2.imwrite('ex3_4.png', img3_4)

課題3

課題1で作成した画像を元にして、赤・緑・青の各成分を2段階に減色した画像 ex3_2.png, 4段階に減色した画像 ex3_4.png を出力してください。
※ ex3_2.png よりも ex3_4.png の方が元の画像の色に近くなるはずです。

4. クラスタリングを使った減色

動画の解説を参照

前項の方法でそれぞれ8色、64色に減色した画像を作りましたが、色数を増やしてもそれほど元画像の色には近くなりません。
これは、「使わない色の絵の具」を増やしてしまったせいです。
k平均法 (参考) では、画像に存在するピクセルを色空間内に配置してその塊 (クラスター) の中心を「絵の具」として使います。
結果として元画像で多く使われている色に近い結果が得られます。
下のコードの?が減色後の色数です。
from sklearn.cluster import KMeans
img4 = img1.copy()
model = KMeans(n_clusters=?)
X = img4.reshape((new_w * new_h, 3))
model.fit(X)
KMeans(n_clusters=8, random_state=0)
Y = model.predict(X)
centers = np.zeros(model.cluster_centers_.shape, dtype='uint8')
for i in range(0, model.cluster_centers_.shape[0]):
  temp = centers[i] = model.cluster_centers_[i]
  for j in range(0, model.cluster_centers_.shape[1]):
    centers[i][j] = int(temp[j])
img4 = np.empty((0, 3), dtype=np.uint8)
for i in range(0, Y.shape[0]):
  img4 = np.append(img4, np.array([centers[Y[i]]]), axis=0)
img4 = img4.reshape((new_h, new_w, 3))
cv2.imwrite('ex4.png', img4)
下の画像はk平均法でそれぞれ8色, 64色に減色したものです。前項の同じ色数の2つの画像と比べると、元画像に近い色になっていることがわかります。

課題4

課題1で作成した画像を元にして、k平均法で減色した画像 ex4.png を出力してください。
※ 次の項で枠と合成することを考慮して色数を決めてください。

5. 画像の合成

動画の解説を参照

前項の方法で得た減色画像 (img4) に、第2項で得た太枠の画像 (img2_5) を合成すれば、漫画のような画像ができます。
そのために、まずグレースケール画像である img2_5 から、カラーの img4 と同じ形式の画像 img_frame を作ります (色は白黒のままで、形式を変換するだけです)。
img_frame = cv2.cvtColor(img2_5, cv2.COLOR_GRAY2BGR)
枠の色が白なので、これらをそのまま合成するとこのようになってしまいます。

そこで、「img4 をベースとして、img_frameの同じ位置の点を調べ、その点が白なら出力画像のその位置を指定の明るさの灰色にし、そうでなければなにもしない」という処理を行います。
ここで使用するwhere関数の第3引数はベースとする画像、第1引数は変更を行う条件、第2引数は条件を満たしたときに変更する色です。
img5 = np.where(img_frame == 255, ?, img4)
第2引数を 128, 0 にするとそれぞれこのような画像ができます。
img_frame = cv2.cvtColor(img2_5, cv2.COLOR_GRAY2BGR)
img5 = np.where(img_frame == 255, ?, img4)
cv2.imwrite('ex5.png', img5)

課題5

課題2で作成した太枠画像、課題4で作成した減色画像を元にして、それらを合成した画像 ex5.png を出力してください。
※ where関数の第2引数 (枠の色) は元画像の状態に合わせてふさわしいものにしてください。

提出

動画の解説を参照

今回作成したノートブックを「imgprc2024@gmail.com」と共有してください。
※ 第2回で共有した、画像共有用の「画像処理」フォルダの中に「第8回」フォルダを作り、その中に今回の元画像と出力画像を入れてください。
※ 課題の再提出の際は、ノートブックの再度の共有はせずにチャットで連絡してください。