1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

numpyで画像の二重の重ね合わせ処理をする ~透視メガネを作る~

Last updated at Posted at 2021-08-15

はじめに

透視メガネは男のロマン。ドラえもんを思い浮かべるかTo LOVEるを連想するかインチキ通販の苦い思い出を蘇らせるかは人によって異なる。
これをPythonで実装してみよう。といってもディープラーニングを駆使して着衣の奥にある見えない裸体をAIに生成させるわけではなく、あらかじめ脱衣画像を用意しておく必要があるわけだが。空間を切り裂くプログラムとほぼ同内容だ。

多くのテレビゲームには登場キャラクターの立ち絵がある。恋愛要素のあるアドベンチャーゲームなどでは制服や私服や水着で同一のポーズの画像が用意されていることも少なくない。空間を切り裂くプログラムではそういうのを使ってアウトプットを作りたかったのだが、残念なことにそのようなゲームを持っていなかった。当時は。
しかし今は違う。今の私はかわいい子をひん剥くことができる。
かわいい子の名前はリンク。そう、ブレスオブザワイルドの。去年の12月にようやくSwitchを買ったのよね。

着衣 linkA.jpg 脱衣 linkB.jpg
linkA.jpg linkB.jpg

ソース

透視を実装するにはスプライトの処理を2回おこなうのとは別の、二重の重ね合わせが必要だ。
それを実装するにあたりこれまでスプライトの勉強でこだわってきたマスク処理でなくnumpy.where()を使うことにした。これならば複数の透明色を設定できる。
ちなみに特撮ではブルーとグリーンのいる戦隊ヒーローは以下のような苦労をしているそうな。

透視メガネの画像は自分で作った。アニメアニメした画像で同様のことをする際は別の色にしたほうがよいが、その際は注意が必要だ(後述)。

glass.png
glass.png
sukesuke.py
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()

これにより以下のようにじっくりねっとりと透視メガネを楽しむことができる。

結果
link.gif

疑問

ところが、透視メガネの色を変更させると期待通りに動かなくなってしまうことがある。もちろん最新のnumpy==1.21.1での話だ。
これは一体どういうことだろう(まさかouter_colorinner_colorの変更を間違えているということはないと信じたい)。

sukesuke.pyの一部を変更

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
glass tmp1(途中の姿) tmp2(最終の姿)
glass.png
背景=(255,255,255)
レンズ=(0,0,0)
縁=(208,99,244)
link11.png
背景:正しく着衣
レンズ:正しい色
縁:正しい色
link12.png
背景:正しく着衣
レンズ:正しく裸
縁:正しい色
link30.png
背景=(255,255,255)
レンズ=(0,0,0)
縁=(0,255,0)
link31.png
背景:正しく着衣
レンズ:正しい色
縁:合成
link32.png
背景:正しく着衣
レンズ:正しく裸
縁:合成
link20.png
背景=(255,0,0)
レンズ=(0,0,255)
縁=(255,255,255)
link21.png
背景:正しく着衣
レンズ:合成
縁:合成
link22.png
背景:合成
レンズ:正しく裸
縁:合成

リンクきゅんの股間ばかり注視してしまいたいへん恐縮だが、この辺りがもっとも違いが分かりやすいので勘弁してほしい。
**numpy.where(condition, x, y)**はconditionによってxもしくはyのどちらかを返すという関数のはずなのだが、なぜかxyの演算が発生してしまっているようだ。
本件について知見のある方はぜひ教えてください。

終わりに

世間一般から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() glass.png link_bust.png temp11.png temp12.png
座標
(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()
glass.png link_bust.png temp21.png temp22.png
備考 outer_color =
(255,0,0)
期待通り 期待通り

ソース改

変更部分は多くないがコピペしてすぐに遊べるよう全ソースを示しておく。

sukesuke.py
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()は知らなかったので正確には適切な検索ワード)を思いついた。
環境が変わると思考も変わって解法にたどり着くことができるようになるという実例でもある。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?