OpenCVでMONO消しゴムのデザインをイタリアの国旗にする

~あらすじ~
道のり① 消しゴムの青いところを取得
道のり② MONO消しゴムを判別
道のり③ MONO消しゴムをイタリア化
結果
コード
参考・その他

・~あらすじ~

 クリスマスではないけど、一応アドベントカレンダーの名目で二日目の記事書かせてもらいますOUCCの2DCG班長です。最初は適当にunityの音声認識の記事でも書いてやり過ごそうと思ってたんですが、一日目の先代の部長が結構頑張ってたんで、それを見て急遽書く内容変更して、一日で出来るけどそこそこ難易度のあるテーマを考えました。シートン学園を見ながら3時間くらい悩んだ結果、PCの隣にあったMONO消しゴムのデザインがエストニアの国旗に似ているな~とふと気が付いて、現在に至ります。


・道のり① 消しゴムの青いところを取得

 コードが300行あって説明が大変なので要点をかいつまんで書きます。読んでも幸せになれない可能性があるので、青い鳥をお探しの方は結果まで飛ばした後、AMAZONにお買い求めください。

※実行環境はwindows10、Python3.8で、使用ライブラリはOpenCVとNumPyです。

・MONO消しゴムが写っている画像を読込み

・読み込んだ画像をガウス平滑化

・cv2.inRangeで青周辺の色域選択&選択部分取得

・cv2.dilateで白い部分を膨張させ、erodeで収縮

↑(これは白い部分の中に散見される黒い所を出来れば除去したいなという希望でやってます。)

・cv2.findContoursで輪郭を取得

・各々の輪郭についてcv2.contourAreaで輪郭領域内の面積を求め、小さすぎる領域や、大きすぎる領域を除外。

・一面真っ黒の画像をnp.zerosで生成。

・各輪郭について黒一色の画像にcv2.drawContoursで輪郭を描画

・cv2.momentsで輪郭で囲まれた各領域の重心を求める。

・cv2.floodFillで先ほど輪郭を描画した画像について重心を含む領域を塗りつぶし。

↑領域の中の黒い点を塗りつぶしで除去

・塗りつぶし画像のnp.averageが大きければ塗りつぶし失敗なので、cv2.bitwise_notで反転。

・こうして作成された塗りつぶされた画像(輪郭の数だけ存在)と輪郭情報をペアでリスト(名前:blue_area)に格納。

・消しゴムの黒い部分に関しても同様にして、リスト(名前:black_area)に格納。

輪郭を描画した画像

・道のり② MONO消しゴムを判別

MONO消しゴムは青から少し離れて黒の領域があるという性質を利用して消しゴムの柄の場所を特定していきます。

・black_areaとblue_areaの要素の全ての組み合わせについて、
     
  1. ①2つの塗りつぶされた画像の膨張を前述のdilateで行う。
  2.   
  3. ②cv2.bitwise_andで重なった領域を取得する
  4.   
  5. ③2つの画像の重なった領域がnp.averageが0より大きいかを見て存在を確認し、存在すればその組み合わせを除外(一つの消しゴムの青い領域と黒い領域が接することは無いため。)
  6. ④np.concatenateで2つの輪郭領域情報のリストを結合する。

  7. ⑤cv2.minAreaRectで二つの領域に外接する最小の長方形を取得。
  8.   
  9. ⑥2つの塗りつぶされた領域の面積を前述の方法で求める。
  10.   
  11. ⑦長方形の面積を求める。
  12.   
  13. ⑧長方形の面積が塗りつぶされた面積に対し比較的大きかったり小さければその組み合わせを除外(青と黒の領域が離れている組み合わせを除外)
  14.   
  15. ⑨除外されずに残った組み合わせの先ほど作成した長方形を保持。
青い部分の抽出された画像(blue_areaの要素の一つ)と黒い部分の抽出された画像(black_areaの要素の一つ)をOR演算で合成し、外接矩形を表示した画像。心の清い人には外接しているように見えるはず。長方形の面積が白い部分の面積より比較的大きいため、この組み合わせは正しくないと判別する。

・保持された全ての長方形と保持された他の全ての長方形との組み合わせしていき、一方の長方形がもう一方の長方形にほとんど含まれているとき、大きい方の長方形を除外。

↑この判別法は、長方形の領域を塗りつぶし、AND演算を行って重なった部分の比率がどれくらいあるかで判別しています。

・残った領域一つ一つについて、その領域の部分だけをマスキングして切り取る。(読み込んだ画像と長方形の中身を塗りつぶした画像のAND演算)

↑なお、この過程で次元の低い長方形塗りつぶしの画像はcv2.COLOR_GRAY2BGRで3次元に変換。

画像にマスキングをかけて消しゴム一個だけ取得した画像

hitomatagiさんのコードにかけて特徴量マッチングし、コード中のgoodの個数がある一定水準を超えないものを除外。

特徴点を結んでいった画像

ここまでクリアしたものををMONO消しゴムと認定します。


・道のり③ MONO消しゴムをイタリア化

ここから色付けをしていきます。...とその前にもう3時近いので寝ます。おやすみなさい~

おはようございました。現在12時です。どうやら記事を書いていたのは夢落ちではないそうですね。さて...

・各長方形を塗りつぶした画像と読み込んだ画像の青い部分を取得した画像のANDを取る。

・AND演算して出来た画像の白い部分が存在するピクセルの座標と、同じ座標の読み込まれた画像の場所の色を緑に変更。(2重ループ文で一つ一つのピクセルを処理)

・同様にして黒の部分を赤に変更

完成!


・結果

イタリアと化したMONO消しゴム
他の画像での実行結果

 とても精度が悪いですね。影は仕方ないとしても、文字の部分や、ノイズは頑張ればどうにかなる部分です。でも作者は途中で力尽きました。許して下さい。


・コード

RTAして書いたコードなので絶対見ない方がいいですよ。呪われます。コード整理をしてなくてもOKで、呪い耐性がある方だけどうぞ。


import  cv2
import  numpy as np
import compare #引用したコードを記載した場所

def main():
    sample=cv2.imread("./data/monoEraser.jpg")
    testImg=cv2.imread("./data/test1.jpg")
    height, width, channels = testImg.shape
    image_size = height * width
    testImg_b= cv2.GaussianBlur(testImg, (9, 9), 2)
    hsv=cv2.cvtColor(testImg_b,cv2.COLOR_BGR2HSV)
    lower = np.array([110, 50, 50])
    upper = np.array([240, 255, 255])
    frame_mask = cv2.inRange(hsv, lower, upper)  

    img2,blueArea = getColorArea(frame_mask, testImg)
    lower = np.array([0, 0, 0])
    upper = np.array([180, 255, 50])
    frame_mask2=cv2.inRange(hsv,lower,upper)

    kernel = np.ones((5, 5), dtype=np.uint8)

    frame_mask2 = cv2.dilate(frame_mask2, kernel)
    frame_mask2 = cv2.erode(frame_mask2, kernel)


    testImg_g=cv2.cvtColor(testImg,cv2.COLOR_BGR2GRAY)

    img_canny=cv2.Canny(frame_mask2,300,400)


    img,blackArea=getColorArea(frame_mask2, testImg)
    rects=[]
    for i, blue_a in enumerate(blueArea) :
        for j,black_a in enumerate(blackArea):
            b,rect=checkAreaRatio(testImg,blue_a,black_a)
            if(b):
                rects.append(rect)
    deleteRects=[]
    for i, rect1 in enumerate(rects):
        for j,rect2 in enumerate(rects):
            if(i>j):
                k= checkContain(rect1,rect2,height,width)
                if(k==1):
                    deleteRects.append(rect2)
                elif(k==2):
                    deleteRects.append(rect1)
    for rect1 in deleteRects:
        rects.remove(rect1)
    deleteRects=[]
    for rect1 in rects:
        if(not compareTrait(sample,img,rect1)):
            deleteRects.append(rect1)
    for rect1 in deleteRects:
        rects.remove(rect1)
    for rect1 in rects:
        img=changeColorItaly(img,rect1)

    cv2.imshow("sample2", img)
    cv2.imwrite("./data/output6.png", img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
def changeColorItaly(img,rect):
    height, width, channels = img.shape
    mask = np.zeros((height, width), dtype=np.uint8)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    mask1 = np.copy(mask)
    mask1 = cv2.drawContours(mask1, [box], 0, 255, 1)
    mask1 = fillArea(mask1, int(rect[0][0] + 2), int(rect[0][1] + 2))
    mask2 = np.copy(mask)
    mask2 = cv2.drawContours(mask2, [box], 0, 255, 1)
    mask2 = fillArea(mask2, int(rect[0][0] + 2), int(rect[0][1] + 2))
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    lower = np.array([110, 50, 50])
    upper = np.array([240, 255, 255])
    frame_mask = cv2.inRange(hsv, lower, upper)
    lower = np.array([0, 0, 0])
    upper = np.array([180, 255, 50])
    frame_mask2 = cv2.inRange(hsv, lower, upper)
    mask1=cv2.bitwise_and(frame_mask,mask1)
    mask2=cv2.bitwise_and(frame_mask2,mask2)
    mask1=cv2.cvtColor(mask1,cv2.COLOR_GRAY2BGR)
    mask2=cv2.cvtColor(mask2,cv2.COLOR_GRAY2BGR)
    for x in range(height):
        for y in range(width):
            b, g, r = mask1[x, y]
            if (b, g, r) == (0, 0, 0):
                continue
            img[x, y] = 99, 135, 0
    for x in range(height):
        for y in range(width):
            b, g, r = mask2[x, y]
            if (b, g, r) == (0, 0, 0):
                continue
            img[x, y] = 57, 41, 206
    return img

def compareTrait(sample,img,rect):
    height, width, channels = img.shape
    mask = np.zeros((height, width), dtype=np.uint8)
    box = cv2.boxPoints(rect)
    box = np.int0(box)
    mask1 = np.copy(mask)
    mask1 = cv2.drawContours(mask1, [box], 0, 255, 1)
    mask1=fillArea(mask1,int(rect[0][0]+2),int(rect[0][1]+2))

    mask1=cv2.cvtColor(mask1,cv2.COLOR_GRAY2BGR)
    img2=cv2.bitwise_and(img,mask1)

    return compare.compare(sample,img2)

def checkContain(rect1,rect2,height,width):#領域の中に領域があるかチェック
    mask = np.zeros((height , width ), dtype=np.uint8)
    box=cv2.boxPoints(rect1)
    box = np.int0(box)
    mask1 = np.copy(mask)
    mask1 = cv2.drawContours(mask1, [box], 0, 255, 1)
    mask1=fillArea(mask1,int(rect1[0][0]+2),int(rect1[0][1]+2))
    box = cv2.boxPoints(rect2)
    box = np.int0(box)
    mask2 = np.copy(mask)
    mask2 = cv2.drawContours(mask2, [box], 0, 255, 1)
    mask2 = fillArea(mask2, int(rect2[0][0] + rect2[1][0] / 2),int( rect2[0][1] + rect2[1][1] / 2))
    mask3=cv2.bitwise_and(mask1,mask2)
    contours, hierarchy = cv2.findContours(mask3, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

    if(len(contours)<=0):
        return  0
    area3 = cv2.contourArea(contours[0])

    area1=rect1[1][0]*rect1[1][1]
    area2 = rect2[1][0] * rect2[1][1]
    ratio=0.9
    if(area3>area1*ratio):
        return  1
    elif(area3>area2*ratio):
        return  2
    else:
        return 0

def checkAreaRatio(img,mask1,mask2):
    mask1,cnt_mask1=mask1
    mask2, cnt_mask2 = mask2
    kernel = np.ones((5, 5), dtype=np.uint8)
    mask4=cv2.dilate(mask1,kernel)
    mask5=cv2.dilate(mask2,kernel)
    mask6=cv2.bitwise_and(mask4,mask5)

    if(np.average(mask6)>0):#隣り合っている領域を除外
        return False,None
    area1=cv2.contourArea(cnt_mask1)
    area2=cv2.contourArea(cnt_mask2)
    cnt_mask3=np.concatenate([cnt_mask1,cnt_mask2])

    rect = cv2.minAreaRect(cnt_mask3)
    area3= rect[1][0]*rect[1][1]
    if (area3 > (area1 + area2) * 3 or area3<(area1 + area2) * 1.6):#比率で除外
        return False,None
    #box = cv2.boxPoints(rect)
    #box = np.int0(box)
    #img2=np.copy(img)
    #img2 = cv2.drawContours(img2, [box], 0, (0, 0, 255), 2)

    return  True,rect

def getColorArea(frame_mask, draw_img, draw=False):
    ret, testImg_g2 = cv2.threshold(frame_mask, 100, 255, cv2.THRESH_BINARY)

    contours, hierarchy = cv2.findContours(testImg_g2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    height, width, channels = draw_img.shape

    img=draw_img
    if(draw):
        img = cv2.drawContours(draw_img, contours, -1, (0, 0, 255, 255), 2, cv2.LINE_AA)
    cont_area=[]

    for i, contour in enumerate(contours):
        area = cv2.contourArea(contour)
        image_size=height*width
        if area < 500:
            continue
        if image_size * 0.99 < area:
            continue
        epsilon = 0.02 * cv2.arcLength(contour, True)
        approx = cv2.approxPolyDP(contour, epsilon, True)
        if(draw):
            cv2.drawContours(img,[approx], -1,  (255, 0, 255), 2)

        mask = np.zeros((height, width), dtype=np.uint8)
        cv2.drawContours(mask, [approx], -1, (255, 0, 255), 2)
        M = cv2.moments(contour)
        mask= fillArea(mask,int(M['m10']/M['m00']),int(M['m01']/M['m00']))#引数に重心を入れている


        cont_area.append((mask,contour))




    return  img,cont_area
def fillArea(img,startx,starty,color=(0,0,255)):
    channels=0
    height=0
    width=0
    if(img.ndim==2):
        channels=1
        height, width = img.shape
    elif(img.ndim==3):
        height, width, channels = img.shape
    mask = np.zeros((height+2, width+2), dtype=np.uint8)#+2しないとエラー
    if(channels==3):
        pass
    else:
        color=255
    retval, img2, mask, rect = cv2.floodFill(img, mask, seedPoint=(startx, starty), newVal=color)
    if(np.average(img2)>100):#もし塗りつぶしが多ければ
        img2=cv2.bitwise_not(img2)#反転
    return img2

if __name__ == '__main__':
    main()

・参考・その他

・OpenCV 3とPython 3で特徴量マッチング(A-KAZE, KNN):

https://qiita.com/hitomatagi/items/caac014b7ab246faf6b1

・OpenCV 2.2 C++ リファレンス:

http://opencv.jp/opencv-2svn/cpp/index.html

・OpenCV-Python チュートリアル :

http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/index.html

筆者:OUCC 2DCG・AI班長 上月

Unityでノベルゲームを制作中

はじめに

この記事はOUCC Advent Calender 2019の23日目の記事です(完成したのは12/31です)

パワポケから野球要素を抜いたノベルゲームみたいなのを作りたかったので、12月から土台作りを始めました。
土台作りといっても基礎となる部分は以下のサイトを参考に作成しました。

今回はこれをパワポケ風に近づけるために追加した機能を書きたいと思います。

1 キャラの立ち位置を二か所にする

パワポケでは左右にキャラが立って、会話が展開されるので、まずはキャラを左右に立たせるようにしました。このとき、3人目のキャラを追加するときは、既にいるどちらか二人のうちの一人を消して、3人目のキャラを追加するようにしました。

Gif画像をUpしたかったのですが、容量的に上げられませんでした...

2 背景画像を変える

#bg_imageで背景を変えられるようにしました。

3 真ん中に画像が出るようにする

背景と同じ要領で真ん中に画像が出るようにしました。背景との違いは#center_imageで真ん中に画像が出現して、#center_image_offでその画像を見えなくするといったところです。

4 選択肢の内容をメッセージボックスに表示する

選択肢のボタンを表示させて、ボタンに触れるとそのボタンのテキストの内容をメッセージボックスに表示するようにしました。

5 その他機能

そのほかに追加した機能としては、BGMを変える機能や、SEを鳴らす機能、コマンドで使用している'#'をメッセージボックスに表示させるためのエスケープシーケンスを実装しました。

6 まとめ

以上述べたような機能を実装しましたが、パワポケ風にするためには、パラメーターの追加やフラグ管理など、追加しなければならないことが沢山あるのでこれからも追加していきたいと思います。
今回は容量の都合上、gif画像を使えなかったので、次に投稿するときはgif画像を上げられるようなブログで書きたいなと思います。

お借りした素材

キャラクターの素材:いらすとや様
背景素材:あやえも研究所様
出演:部室に侵入した猫様

Unityで複数のスクリプトを同様に扱いたい

・あらすじ 

~ Unityで武器換装機能を実装したい~

 先週VRのシューティングゲームを作っている最中に武器の換装機能を徹夜で実装する機会がありました。

 その際プレイヤーやどこかのGameObjectに全ての武器の処理を書くのが煩雑で嫌だなと思ったので、武器のオブジェクトにその武器の動作を規定するスクリプトを張り付けることにしたんです。

 Oculus IntegrationアセットのOVRGrabberスクリプトのgrabbedObjectに現在つかんでいるオブジェクトが格納されるそうなので取得した武器オブジェクトからGetComponentでスクリプトを取得したいと思っていたんですが、unityの仕様上それぞれの武器につけるスクリプト名は同一の名前を使用出来ないので GetComponent で一様に処理できないことに気が付いて、AM2時を迎えた私は大変困りました。

 それで何分悩んだかわからないけど死にかけの頭で考えて何とか実装には成功したので、今後同じ事態にはまった時のためにメモとしてここに記述します。

・ GetComponents

 GetComponents<>()というメソッドがあって、ここでMonoBehaviourを指定すればMonoBehaviourを継承したスクリプト、つまり自作のスクリプト(の大半)を全て取得できるそうです。

public class TimerManager : MonoBehaviour 

  (↑実はこの:の後の部分が継承元だったんですね。私はC#を勉強せずにUnity書いてる人なので知りませんでした。) 

 しかし、MonoBehaviourを指定して取得したインスタンスは MonoBehaviour に存在するメソッドしか使用できないみたいです。

・解

 よって次のように MonoBehaviourを継承した、使用したいメソッドを記述したクラスを生成し、

public class guns : MonoBehaviour
 {
  public virtual void shoot(){}
}

 さらにそれを武器の動作を記述したクラスが継承すれば、 GetComponents<guns>() で取得し、shootメソッドを使用することが出来るようになります。やったね。

public class gun_makrov : guns//継承
{
   public virtual void shoot(){
   //内容
  } 
}

筆者:OUCC 2DCG班班長、AI班班長

ちなみに筆者は当日実装間に合ってません。

ちょこっと思いついてちょこっと作ってみたゲームについて

これはOUCC 2019年アドベントカレンダー 12/10 分の記事です。

ちょこっと思いついてUnityで簡単に作ったゲームについて書きます。作ったといっても個人で自己満足するくらいの出来で、とても他人に見せられるものではないです。

今回考えたゲームのテーマは、「デッキ作製系リアルタイム対戦ゲーム」です。戦闘画面のイメージは次の通り。

対戦画面

画面下にコマンドを10個程度用意し、ボタンをクリックするとその効果を発揮する、という感じで先頭を進めます。連打は禁止するために一定時間押せないようにし、また、押すたびにそのボタンが発揮する効果を変更します。ほんとはもっとカードっぽいものにしたいんですけど、もちろん著者にそんな技術や時間はありません。

ボタンに現れる効果は、あらかじめデッキを作成しておき、そのデッキにおけるカードの割合で出す、ということを考えています。つまり、攻撃カード、攻撃力アップカード、魔法攻撃カード、魔法攻撃アップカードを各1枚ずつ入れたデッキなら、4分の1の確率で各カードが登場します。

戦闘はこのボタンクリックのみで行います。そのため、図では攻撃系のカードしか表示してませんが、体力回復、防御力アップなどのカードも入れる必要があると思います。カードゲームをしたことがある人なら、使いたいカードをただひたすら入れてもうまく動かないことがよくわかると思います。なので、戦闘そのものより、戦闘しやすいようにデッキを作る、というのが主なゲームになると思います。そんなところまで作ってませんけど。

ここからは、このゲームを作る場合どうするのがいいかということついてだらだら書こうと思います。

まず能力アップ系のカードについてですが、戦闘中に何回も押されるため、効果を小さくする必要があります。1.1^100は約14000なので、まあ1.1倍とかがいいのかなと思います。また、1回の戦闘で終わるのはせっかく強くしたのに、という感じだったり難易度的にもその敵に対しデッキを作るだけになるので、フェーズの形で1つ前の状態を受け継いで次の敵キャラとの戦闘に入る、とするほうがいいんじゃないかと思ってます。

デッキ作製系ゲームを謳うならカードの種類もいろいろ必要だと思うんですけど、上に挙げたもの以外だと普通の攻撃だけでなく必殺技を設けるとか、あとはレベルの高いカードを用意して、効果が1.1倍でなく1.2倍になったり、次にカードが押せるようになるまでの時間が短くなるみたいな効果を追加したらいいんじゃないかと思います。あとは、デッキを2つ用意しておいてデッキを入れ替えるカード、というのもゲーム性が上がるんじゃないかと思います。

ここまでいろいろ書いてきましたが、僕はこのゲームを最後まで作るつもりはありません。そんな気力がないので。なのでここまで作って完成ということにします。すでにこういうゲームがあるなら教えてほしいです。