Pillowとnumpyでオリジナルな画像加工を行う
はじめに
画像にマイナーであったりオリジナルな加工や効果を処理したいというときに、 メジャーなPython画像処理ライブラリであるopenCVやPillowに手ごろな関数がなくて困るということがよく発生します。 今回はそのような場合の解決策のヒントとなるようないくつかを紹介します。
今回使ったコードはgistにアップしています。
最初に前処理として減色処理を行いました。 減色処理自体は省略してもいいですが、 目的の処理を行うときにmedianやmodeを使用したい場合はあった方がいいかもしれません。
def quantize(img, colors=64):
return img.quantize(colors=colors, method=Image.MEDIANCUT, dither=Image.FLOYDSTEINBERG).convert("RGB")
img = Image.open('./bird.jpg')
img = quantize(img)
画像に点を並べて模様を作成
何かしらの画像またはブランクのキャンバスに点をいくつも並べ、その点を中心として幅優先探索を使用しピクセルごとに一番近い中心点で領域を分割して 、その領域ごとに着色等の加工を行うことで模様を作成することができます。 この点の並べ方によってハニカム模様やチェック模様をはじめとして多様な模様を作成することができます。
幅優先探索は画像ピクセル数NにたいしてO(N)ですのでPythonなどの遅い言語に対しても高速です。 今回はPillowやnumpyを中心に使用しますが(一部openCVやscipyを使用)、Pillowの部分は代わりにopenCVに置き換えても実現は可能です。 ですが本質部分ではできるだけこれらの画像処理ライブラリの特別な関数は使用しません。
ハニカム模様
まずはハニカム模様を作成し、それを基にして画像を加工していきます。 ハニカム的な画像加工するにはarrayのスライスを使用できないのでそれなりに面倒です。
ハニカム模様にする場合は領域の形が六角形になるように点を並べます。 点を偶数行と奇数行で場合分けをして六角形の一辺の長さ分をずらしながら等間隔で並べていきます。
pixels = [[-1] * w for _ in range(h)]
from collections import deque
que = deque()
dh = n / 2
dw = n
r = 0
cnt = 0
while r * dh < h:
y = int(r*dh)
c=0
while c*dw*2 + r%2*dw < w:
x = c*dw*2 + r%2*dw
pixels[y][x] = cnt
que.append((cnt, y, x))
cnt += 1
c += 1
r += 1
領域分割をするには幅優先探索を使用します。
d = [0, 1, 0, -1, 0]
while len(que) > 0:
id, y, x = que.popleft()
for dy, dx in zip(d[:4], d[1:]):
ny = y + dy
nx = x + dx
if ny >= 0 and ny < h and nx >= 0 and nx < w and pixels[ny][nx] == -1:
pixels[ny][nx] = id
que.append((id, ny, nx))
後は加工したい画像を使用して、ハニカム模様化します。 今回はそれぞれのセルに対する色の決め方はその領域の平均にしました。 上にあげたコードで領域ごとにindex化しているので、img[(mask==i)]で色値を代入できます。 他の色の決め方の例としては中央値(median)や最頻値(mode)などが候補になります。
def make_honycomb(img, num):
img = np.asarray(img)
h, w = img.shape[:2]
mask, cnt = honeycomb(h, w, num)
mask = np.array(mask)
for i in range(cnt):
val = np.mean(img[(mask==i)], axis=0)
img[(mask==i)] = val
return Image.fromarray(img.astype(np.uint8))
点の並べ方を変えるとひし形のチェッカー模様なども作成することができ、 またドロネー三角形とボロノイ図は双対なのでドロネー三角形の頂点を中心とすればボロノイ模様を作成することができます。 点の間隔を変えると領域の粒度も変えられますし、他にもいろいろアレンジの余地はあります。
ランダムに点をばらまいて絵画風の画像にする
ここではランダムに点をばらまいて模様を作成することで絵画風の画像にすることもできます。 openCVにも油絵加工処理フィルターがありますが、またそれとは違った感じのものになります。 アレンジとしては点の数を変えることで画像のボケ具合が変わりますし、ランダムの分布を変えればまた違った印象の画像になります。 ランダム点を設定するには、先ほどのハニカム模様のコードの点の追加の部分を以下に変更すればいいだけです。 ここではrandrangeを使用します。 randrangeはrandintと違って半開区間になるので(randrange(a,b)ならば[a,b))なのでrangeと同じ仕様で分かりやすいです。
for i in range(n):
while True:
r = randrange(0, h)
c = randrange(0, w)
if pixels[r][c] == -1:
pixels[r][c] = i
que.append((i, r, c))
break
ドット絵風の画像に加工
ひし形のチェッカー模様は前述の方法で作成できますが、 ドット絵のような正方形が傾いていないチェッカー模様の加工は幅と高さで範囲を指定してスライスごとに処理します。 その後スライス枠内をnp.meanで色の平均をとってその色を代入します。 似たような処理はリサイズを縮小・拡大を行うだけでも可能ですが、こちらの方法は着色方法等にアレンジが利きます。
def dot_effect(img, sz):
img = np.asarray(img)
h, w = img.shape[:2]
th = (h+sz-1)//sz
tw = (w+sz-1)//sz
for i in range(th):
for j in range(tw):
val = np.mean(img[i*sz:min(h,(i+1)*sz), j*sz:min(w,(j+1)*sz)], axis=0)
img[i*sz:min(h,(i+1)*sz), j*sz:min(w,(j+1)*sz)] = np.mean(val, axis=0)
return Image.fromarray(img.astype(np.uint8))
またnumpyのmeshgridを使用して画素のindex情報を用いながら画素ごとに処理を行うことでメッシュ処理も行います。
def mesh(img, r, c, v, h, w, ksize):
return 0 if r%ksize==0 or c%ksize==0 else img
def vec_effect(img, f, ksize=None):
img = np.asarray(img)
h, w = img.shape[:2]
r, c, v = np.meshgrid(np.arange(img.shape[0]), np.arange(img.shape[1]), np.arange(img.shape[2]), indexing='ij')
v_mesh = np.vectorize(f)
img_mesh = v_mesh(img, r, c, v, h, w, ksize)
return Image.fromarray(img_mesh.astype(np.uint8))
このmeshgridを用いた処理は他にも応用可能です。 sinカーブなどを使用すれば以下のような画像加工もできますし、 いろんな方向や部分に対して多様なgradient処理なども行うことができます。
ドロネー三角分割を用いて画像加工
ここではscipy.spatialのDelaunayを使用します。 ドロネー三角分割はモデル対象物をポリゴン化するときなどに使用されます。 ドロネー三角形の頂点を取得できればcv2.fillpolyを使用してそれぞれの三角形にインデックス付けを行い、 それぞれの三角形ごとに処理を行うことが可能になります。
def delaunay_divide(img, n=256):
h, w = img.size
points = set()
points.add((0,0))
points.add((h,0))
points.add((h//2,0))
points.add((0,w))
points.add((0,w//2))
points.add((h//2,w))
points.add((h,w//2))
points.add((h,w))
for i in range(n):
while True:
r = randrange(0, h)
c = randrange(0, w)
if (r,c) not in points:
points.add((r,c))
break
img = np.asarray(img)
points = list(points)
triangles = Delaunay(points).simplices
mask = np.zeros((w, h), dtype=np.int32)
for i, ti in enumerate(triangles):
pts = [points[ti[0]],points[ti[1]],points[ti[2]]]
cv2.fillPoly(mask, pts=[np.array(pts).reshape((-1,1,2)).astype(np.int32)], color=i)
for i, ti in enumerate(triangles):
#青緑っぽいランダム色(RGBA)
val = (100,randrange(100,255),randrange(100,255), 255)
#画像ベースで着色
#val = np.mean(img[(mask==i)], axis=0)
img[(mask==i)] = val
return Image.fromarray(img.astype(np.uint8))
ドロネー分割を行った画像を取得した後に、 以前のブログで作成した背景除去の手法を用いて取得した背景除去マスクを使用して、 以下のような画像を作成しました(本ブログ冒頭画像と同じ)。
以前のブログはhttps://tk87s.blogspot.com/2022/03/openccv-deeplearningsemantic.html
ドロネー三角分割ができればボロノイ分割への加工も容易となります。
まとめ
今回はニッチなオリジナル画像加工の例を紹介しました。 オリジナルの処理では既存のライブラリを使用できないケースが出てくると思います。 普通Pythonでピクセルごとに計算を行うと処理に時間がかかりますので、躊躇してしまう部分があったりしますが、 numpyを使用すれば高速に処理を行うことが可能ですので、積極的に使用したほうが楽しいかと思います。
次の機会ではRustを使用して画像処理を行います。 Rustは画像処理ライブラリはPythonほど充実していませんが、高速にピクセルの処理を行うのに向いています。