catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

カードゲームARの実現可能性検証

なんとなくですが実現可能性は高い気がしてきました。かたりぃなです。 本題に入る前に問題領域を今一度整理します。

やりたいこと

MTG遊戯王みたいなカードゲームの遊びの付加価値として、AR化を考えています。 やりたいことはARと一言で済むのですが、もう少し問題領域を整理します。

  • 現実空間のカードを自動的に検出できる
  • 検出したカードが何なのかを言い当てられる
  • カード上に任意の3Dモデルをレンダリングできる

こんなところです。

このうち「現実空間のカードの検出」が今回のエントリでのお題です。

カードが何なのかを言い当てるのは、以前のエントリで実現可能性は見えてきているので今回は触れません。

3Dモデルのレンダリングは、あとでやります。モーションやエフェクトも考えるとUnity使ったほうがラクかなぁと思っています。

肝心の3Dモデルデータはありません。mod方式にしておいてユーザーが差し替えられる仕組みがあれば楽しめそうです。最初はコナンの犯人シルエットみたいなのでも出しておくつもりです。もしモデル作ってる人がいたら、そのとき考えることにします。

概念図

Hololensだけではマシンパワー不足なので、PCのパワーを借りることにします。 実際に実現できて採算がとれるならクラウドでやればいいですし。

気分転換に概念図書いてみました。赤文字が今回やるところです。 f:id:Catalina1344:20170720223327p:plain

既存のライブラリで不足していること

Hololens単体でも、Vuforia使っても、私のやりたいことには単体では届きません。

まずそれぞれでできることは

  • Hololensは空間認識ができる
  • Hololensは自分の位置を知っている
  • Vuforiaは検出対象物が既知でなければならない

です。 Vuforiaを試そうかと思いましたが、全部のカードを登録していくのは少々面倒です。ほかのARライブラリについても同様です。

じゃあHololensなら解決できるの?と問われると、そんなことは全然なくて、あくまで空間と自身の位置を認識することで、その空間に任意の3Dレンダリングを行えるだけです。

というわけで、検出対象物の検出をどう工夫するかといったところが今回のお題です。

R-CNN

領域抽出をいろいろと調べて回りました。

ざっと年代順にR-CNNの技術要素を並べると

  • R-CNN
  • fast R-CNN
  • faster R-CNN
  • YOLO
  • SSD

こんな感じのようです。 古いものは領域抽出自体はselective searchで、その領域の分類をCNNでやっているだけの雰囲気でした。(ざっと読んだだけなので違うかもしれません)

SSDは「物体の領域抽出と」「物体のカテゴリ分類」を一度のCNN計算で済ませることからついた名前らしいです。(Single Shot Detect)

実際にくっつけてみる

R-CNN自体は以前のエントリでchainercvを使って試したので、今回も同様にしてみます。

chainercvの実装が変わったのか、うまく検出できなくなっているのでスコアが低いオブジェクトも列挙できるようにuse_presetで閾値をさげておきます。

このモデルは自分の目的にあわせて学習させていないので、あとで少しずつ方法論を考えます。

rcnn_model.predict()で領域抽出してもらいます。 一度に複数枚の画像を入力できるらしく、引数はリストです。 戻り値も当然リストなので0番目の要素をとっておきます。 bboxが傾いていないバウンディングボックスで、labelがその領域のカテゴリです。

    rcnn_model = SSD300(
            n_fg_class=len(voc_detection_label_names),
            pretrained_model='voc0712')
    chainer.cuda.get_device(0).use()
    rcnn_model.to_gpu()
    rcnn_model.use_preset('evaluate')

    # R-CNNへ入力してobject-proposalをする
    bboxes, labels, scores = rcnn_model.predict([pixels])
    bbox, label, score = bboxes[0], labels[0], scores[0]

バウンディングボックスをもとにcropする

バウンディングボックス内にカード画像が含まれているとするなら、そのバウンディングボックスのROIに対してカード検出をかければいいと考えます。 というわけで、バウンディングボックスのROIを作ります。ここからopencvの世界です。

    for i, b in enumerate(bbox):
        top, left, bottom, right = b
        t = 0 if top < 0 else top
        l = 0 if left < 0 else left
        croped_img = base_img[int(t):int(bottom), int(l):int(right)]
        croped_img_clone = np.array(croped_img)

カードを検出する

以前c++でやったことをpythonで実装します。 実装し終わって気づいたのですが、C++で作った部分をdll化してCTypes使えば済んだかもしれません。

夏バテなのか、疲れがたまっているのか、コード汚いです。動けばいいやというスタンスです。

def card_detect(img):
    # 平滑化、二値化、輪郭抽出
    g_img = cv2.GaussianBlur(img, (5,5), 8)
    r, bin_img = cv2.threshold(g_img, 85, 255, cv2.THRESH_BINARY_INV)
    canny_img = cv2.Canny(bin_img, 50, 200)

    # 確率的ハフ変換による線分検出
    h, w, c = img.shape
    minLineLength = min(w,h) // 4
    maxLineGap = 10
    lines = cv2.HoughLinesP(canny_img, 1, np.pi/180, 40, minLineLength, maxLineGap)
    if lines is not None:
        if len(lines) < 4:
            return None

        # 検出した線分のすべての頂点を列挙
        pts = np.array(lines)
        pts = pts.reshape(-1, 2)

        # 回転を考慮した外接矩形を求める
        rect = cv2.minAreaRect(pts)
        box = cv2.boxPoints(rect)
        box = np.int0(box)

        return box
    return None

この関数は、カードらしい領域が見つかった(=線分が4本以上ある)ならば、傾きを考慮したバウンディングボックスが返されます。見つからなかったらNoneです。

傾きを考慮したといっても、透視投影変換のような変換には耐えられないので、机に対して斜めから撮影した画像ではズレが大きくなります。あとでもうちょっとまともにしましょう。

画像分類するために透視投影変換をする

以前、chainerで画像分類のために学習させたモデルはカードの正位相(MTG用語)での画像です。 なので、正位相の画像を作ります。

(省略)

画像分類する

以前にchainerを使って実装したものそのままです。 このコード自体はいたって普通なのですが、NNから間違えた答えしか返ってきません。悲しい。

なんとなくですが、opencvとpillowで行列の形が違う気がします。特にカラーチャネルとか。 あとで見直しましょう。

def detect_cardtype(image):
    # NNへ入力可能な形式に変換する
    pixels = np.asarray(image).astype(np.float32)
    pixels = pixels.transpose(2, 0, 1)
    pixels = pixels.reshape((1,) + pixels.shape)

    print("NN input image shape = {}".format(pixels.shape) )
    # 識別
    y = model(pixels)
    prediction = F.softmax(y)
    m = np.argmax(prediction.data)
    return m

実行結果

カード検出と分類結果の取得まで1秒くらいで動いています。実測はしていませんが、これくらいの遅延なら実運用に耐えられるかなと思います。

Hololensと連携したい

実際のアプリと連携させるにあたって、この遅延時間を考慮する必要があります。

遅延時間の考慮とはどういうことかというと、ある画像からカードを検出して識別結果が返ってきたとして、それは過去の映像フレーム上での話であってHololensの今現在の状態から過去に-delta遡った時点での推定結果です。

ここでHololensが「自身の空間上の位置と姿勢」を把握していることを活用することになりそうです。

つまり識別したい映像フレームが採取されたタイミングでのHololensの空間上の位置・姿勢を保持しておけば、そこからの相対位置で本来あるべき位置へと補正できるというアイデアです。

DeepLearningで推定されたカードの位置・姿勢は、確かにDNNの計算時間による遅延はあります。 けれども、Hololensがフレームを採取した時点での位置姿勢とは必ず合致するはずです。 ということは、推定された位置と姿勢は現在時刻からみて-deltaの時刻の姿勢にあたるので、このdelta時間の間に変化したHololensの位置・姿勢の変化量を加味してあげれば辻褄はあうはずです。

カード種別間違えて識別しているのはそのうち調査するとして、先にこっちの理屈が正しいかどうかの確認をすすめたいところです。

今後の方針

私は技術的な課題(遅延)を追及することよりも、実用性・コストパフォーマンスのほうが重要だと思っています。 ただし技術を無視していいとは思いません。あくまで優先順位の話です。

なぜなら「技術的・理論的に美しい」ことと、「面白い・実用的なアプリである」というのは別の概念です。

なので「実現可能性があること」と「実現に向けた課題の洗い出し」のためにプロトタイプを作ります。

応用分野

物体の検出と分類がほぼすべてDeepLearningで実現できそうなので、あとはゲームが変わっても学習モデルを更新するだけで済むんじゃないかなと思っています。

汚いコードですがgistにあげておきました。 https://gist.github.com/javoren/2fa2fc6ebcd8582d72b86f7017e742eb

今後の展望

古いPCがお亡くなりになって、夜な夜な貯めていたアレな動画とか重要参考資料がなくなってしまいました。 バックアップ大事。

gistが正しい意味でバックアップといえるかどうかはさておき。

それでは今回はこれくらいで。