はじめに
透視メガネは男のロマン。ドラえもんを思い浮かべるかTo LOVEるを連想するかインチキ通販の苦い思い出を蘇らせるかは人によって異なる。
これをPythonで実装してみよう。といってもディープラーニングを駆使して着衣の奥にある見えない裸体をAIに生成させるわけではなく、あらかじめ脱衣画像を用意しておく必要があるわけだが。空間を切り裂くプログラムとほぼ同内容だ。
多くのテレビゲームには登場キャラクターの立ち絵がある。恋愛要素のあるアドベンチャーゲームなどでは制服や私服や水着で同一のポーズの画像が用意されていることも少なくない。空間を切り裂くプログラムではそういうのを使ってアウトプットを作りたかったのだが、残念なことにそのようなゲームを持っていなかった。当時は。
しかし今は違う。今の私はかわいい子をひん剥くことができる。
かわいい子の名前はリンク。そう、ブレスオブザワイルドの。去年の12月にようやくSwitchを買ったのよね。
着衣 linkA.jpg | 脱衣 linkB.jpg |
---|---|
![]() |
![]() |
ソース
透視を実装するにはスプライトの処理を2回おこなうのとは別の、二重の重ね合わせが必要だ。
それを実装するにあたりこれまでスプライトの勉強でこだわってきたマスク処理でなくnumpy.where()
を使うことにした。これならば複数の透明色を設定できる。
ちなみに特撮ではブルーとグリーンのいる戦隊ヒーローは以下のような苦労をしているそうな。
透視メガネの画像は自分で作った。アニメアニメした画像で同様のことをする際は別の色にしたほうがよいが、その際は注意が必要だ(後述)。
glass.png |
---|
![]() |
import cv2
import numpy as np
def put_sprite(event, x, y, flags, param):
if event == cv2.EVENT_MOUSEMOVE:
img = sukesuke(imgA, imgB, glass, (x, y), home=(130,130))
cv2.imshow(winname, img)
def sukesuke(img1, img2, glass, pos, home=(0,0)):
result = img1.copy()
imgH, imgW = result.shape[:2]
gH, gW = glass.shape[:2]
x, y = pos
xc, yc = home
x, y = x-xc, y-yc
x1, y1 = max(x, 0), max(y, 0)
x2, y2 = min(x+gW, imgW), min(y+gH, imgH)
if not ((-gW < x < imgW) and (-gH < y < imgH)):
return img1
outer_color = (255,255,255)
inner_color = (0,0,0)
img1_roi = img1[y1:y2, x1:x2]
img2_roi = img2[y1:y2, x1:x2]
glass_roi = glass[y1-y:y2-y, x1-x:x2-x]
temp1 = np.where(glass_roi==outer_color, img1_roi, glass_roi)
temp2 = np.where(glass_roi==inner_color, img2_roi, temp1)
result[y1:y2, x1:x2] = temp2
return result
imgA = cv2.imread("linkA.jpg")
imgB = cv2.imread("linkB.jpg")
glass = cv2.imread("glass.png")
winname = "sukesuke"
cv2.namedWindow(winname)
cv2.setMouseCallback(winname, put_sprite)
cv2.imshow(winname, imgA)
while True:
if cv2.waitKey(1) & 0xFF == 27: # esc
break
cv2.destroyAllWindows()
これにより以下のようにじっくりねっとりと透視メガネを楽しむことができる。
結果 |
---|
![]() |
疑問
ところが、透視メガネの色を変更させると期待通りに動かなくなってしまうことがある。もちろん最新のnumpy==1.21.1
での話だ。
これは一体どういうことだろう(まさかouter_color
やinner_color
の変更を間違えているということはないと信じたい)。
def sukesuke(img1, img2, glass, pos, home=(0,0)):
# 中略
outer_color = (255, 0, 0) # 必要に応じて色指定を変更する
inner_color = (0, 0, 255) # 必要に応じて色指定を変更する
# 中略
temp1 = np.where(glass_roi==outer_color, img1_roi, glass_roi)
cv2.imshow("temp1", temp1) # 追加する
temp2 = np.where(glass_roi==inner_color, img2_roi, temp1)
cv2.imshow("tmp2", temp2) # 追加する
result[y1:y2, x1:x2] = temp2
return result
リンクきゅんの股間ばかり注視してしまいたいへん恐縮だが、この辺りがもっとも違いが分かりやすいので勘弁してほしい。
**numpy.where(condition, x, y)
**はcondition
によってx
もしくはy
のどちらかを返すという関数のはずなのだが、なぜかx
とy
の演算が発生してしまっているようだ。
本件について知見のある方はぜひ教えてください。
終わりに
世間一般から4年以上遅れているが、女装リンク、いい…。
追記:疑問解消
numpy.where()
の仕様
どうやらnumpy.where()
は多次元配列では個々の要素について評価がおこなわれるようだ。
といっても具体的にどのような評価をしているのかはよくわからなかった。
numpy.where()
でも期待通りに動くこともあるし。
temp1 = np.where(glass_roi==outer_color, img1_roi, glass_roi)
temp2 = np.where(glass_roi==inner_color, img2_roi, temp1)
glass | 着衣画像 | temp1 | temp2 | |
---|---|---|---|---|
np.where() |
![]() |
![]() |
![]() |
![]() |
座標(50,50) のBGR値 |
レンズの色(0,0,255)
|
マスクの色(188,203,135)
|
なぜか(0,203,255)
|
肌色(131,190,239)
|
備考 | outer_color = (255,0,0)
|
対策 numpy.all()
を使う
(height, width, 3channel)
の3次元配列で最後のBGR要素が等しいかどうかを評価するにはnumpy.all()
を使う。
配列の指定した軸がすべてTrueであればTrueを返す
numpy.all(a, axis=None, out=None, keepdims=False)
配列の指定した軸の少なくとも一つがTrueであればTrueを返す
numpy.any(a, axis=None, out=None, keepdims=False)
- a 配列に相当するもの。
- axis 軸。タプルで複数指定可。省略すると全次元。
-
out 戻り値を出力する変数。省略可能でデフォ値は
None
。普通にresult = numpy.all()
と書けばこんなの不要だと思うんだけど。 -
keepdims 戻り値の次元を元の配列と同じにするかどうか。省略可能でデフォ値は
False
。
公式なドキュメントはこちら。
numpy.where()
ほど知名度が高くない(?)このnumpy.all()
、いくつかのウェブサイトを調べたが教科書通りの使い方しか紹介されていなかった。しかし、これをOpenCVの画像に使うといろいろ面白いことができる。
画像の各ピクセルの色が等しいかどうかを評価する
カラー画像は(height, width, 3channel)
の3次元で表現されるからaxis=2
を指定する。そしてここが肝心なのだが、評価対象は「画像の各ピクセルの色が指定したBGR値に等しいかどうか」とする。つまり、
cond1 = np.all(glass_roi==outer_color, axis=2)
と書けばよい。
戻り値は各要素がTrue
もしくはFalse
の値を持つ(height, width)の2次元配列だ。
評価結果をnumpy.where()
にかける
これを元にnumpy.where()
を使ってピクセルごとに画像Aの色と画像Bの色を振り分けたいところだが、残念ながら2次元配列をもとに3次元配列を振り分けることはできない。
そこで使うのがkeepdims
オプション。これを有効にすることで次元が保たれる。numpyだからnumpy.resize()
を使ってもよいが、このオプションはありがたい。
戻り値は各要素がTrue
もしくはFalse
の値を持つ(height, width, 1)の3次元配列。これでnumpy.where()
を使うことができるようになる。
cond1 = np.all(glass_roi==outer_color, axis=2, keepdims=True)
temp1 = np.where(cond1, img1_roi, glass_roi)
cond2 = np.all(glass_roi==inner_color, axis=2, keepdims=True)
temp2 = np.where(cond2, img2_roi, temp1)
glass | 着衣画像 | temp1 | temp2 | |
---|---|---|---|---|
np.all() + np.where()
|
![]() |
![]() |
![]() |
![]() |
備考 | outer_color = (255,0,0)
|
期待通り | 期待通り |
ソース改
変更部分は多くないがコピペしてすぐに遊べるよう全ソースを示しておく。
import cv2
import numpy as np
def put_sprite(event, x, y, flags, param):
if event == cv2.EVENT_MOUSEMOVE:
img = sukesuke(imgA, imgB, glass, (x, y), home=(130,130))
cv2.imshow(winname, img)
def sukesuke(img1, img2, glass, pos, home=(0,0)):
result = img1.copy()
imgH, imgW = result.shape[:2]
gH, gW = glass.shape[:2]
x, y = pos
xc, yc = home
x, y = x-xc, y-yc
x1, y1 = max(x, 0), max(y, 0)
x2, y2 = min(x+gW, imgW), min(y+gH, imgH)
if not ((-gW < x < imgW) and (-gH < y < imgH)):
return img1
# ここも小改造しています 自作メガネ画像の仕様に準じて色の自動取得をする
outer_color = glass[0][0] # 画像の左上、背景部の色
inner_color = glass[50][50] # 座標(50,50)、レンズ部の色
img1_roi = img1[y1:y2, x1:x2]
img2_roi = img2[y1:y2, x1:x2]
glass_roi = glass[y1-y:y2-y, x1-x:x2-x]
# 主な変更部分はここ
cond1 = np.all(glass_roi==outer_color, axis=2, keepdims=True)
temp1 = np.where(cond1, img1_roi, glass_roi)
cond2 = np.all(glass_roi==inner_color, axis=2, keepdims=True)
temp2 = np.where(cond2, img2_roi, temp1)
result[y1:y2, x1:x2] = temp2
return result
imgA = cv2.imread("linkA.jpg")
imgB = cv2.imread("linkB.jpg")
glass = cv2.imread("glass.png")
winname = "sukesuke"
cv2.namedWindow(winname)
cv2.setMouseCallback(winname, put_sprite)
cv2.imshow(winname, imgA)
while True:
if cv2.waitKey(1) & 0xFF == 27: # esc
break
cv2.destroyAllWindows()
終わりに改
numpy.where()
についてはやや消化不良だが目的が達成できたので良しとしよう。
夏休みが終わって出勤したら突然適切な方法(numpy.all()
は知らなかったので正確には適切な検索ワード)を思いついた。
環境が変わると思考も変わって解法にたどり着くことができるようになるという実例でもある。