Yahoo! Hack Day 2017 に参加しました

12月9日〜10日の2日間、秋葉原のUDXで行われたYahoo主催のハッカソンに参加してきました。

Yahoo! JAPAN Hack Day 10th Anniv. / テクノロジーで遊ぼう!クリエイターフェス×日本最大級ハッカソン、12/9-10に秋葉原で開催。10周年特別版! #hackdayjp

ハッカソン詳細はこちら。

作品について

クラッピー(パチパチトールくん) をテーマに、ダルマさんが転んだを独りでできるソリューションを作りました。

発表時のスライドはこちら

  • タミヤの カムプログラムロボット工作セット : 脚と土台(物理)
  • ESP32 + モータードライバ + NeoPixel : BLEでコマンド受け取ってカムプログラムロボットのモーターや飾りを制御
  • iPhone X : 画像認識, BLEでESP32と接続してコマンド送信, Google Home miniから命令受け取り
  • Google Home mini + node-red : 「ダルマさんが転んだ」を音声認識して命令

完成した無限クラッピー追尾ロボット。
かなりインパクトのある面になりました。

私は、この作品の中で主にiPhoneアプリ(画像認識 + BLE制御)を担当しました。

画像認識エンジンがクラッピーを認識している様子はこちらです。

ちゃんと赤と黄のクラッピーを別々に認識しています。また、横や斜めはもちろん、後ろ姿でも認識します。

なお、画像認識をiPhoneで行った理由は以下の通りです。

  • ハードウェアで推論を走らせることで、ほぼリアルタイムにハードウェアの制御ができる。
  • Movidius Neural Compute Stick + Raspberry Pi3 の構成よりもコンパクト。画面があるのでデバッグしやすい。
  • 固定方法や給電等、周辺のパーツにスマホ用の安い部品を流用できる。
  • 開発がハード担当と制御担当に分担できる。

画像認識について

画像認識には DarknetTinyYOLO を使用しています。



YOLO: Real-Time Object Detection

学習モデルの作り方は基本的には Training YOLO on VOC の項目の通りです。

作業としては、まずは BBox-Label-Tool で教師データを作ります。  

これを darknet を使用して TinyYOLO に学習させます。

TinyYOLOで学習させたWeightsをKerasにインポートして、Keras形式の学習済みモデルとしてh5ファイルで書き出して、 coremltools で CoreML形式のモデル(.mlmodel) に変換してiOSアプリに組み込みます。

iOSアプリでは、Vision framework ( VNCoreMLRequest ) を使う事でカメラからリアルタイムに取得した映像をオンタイムで学習済みモデルによる推論を行う事ができます。

認識した結果は、大まかには 認識したクラスのIndex確率認識した物体のBoundingBox(CGRect) で受け取れます。

そこで、 フレームごとの認識結果を付き合わせて、BoundingBoxが重なるものを同一オブジェクトと見なし、その移動量が閾値を超えた時に 「動いたオブジェクト」 と判断する事で、動体検知を行いました。

各フレームごとの、TinyYOLOの認識結果を受けての上記の処理は大まかには以下のようなものです。

class ViewController: UIViewController {

        /* 〜 中略 〜 */

    var cluppies : [Player] = [] //現在捕捉中のクラッピーのリスト

        /* 〜 中略 〜 */

    /// 同一判断する, 差分確認する, 追いかける
    func detectPlayerAndDiff(predictions: [YOLO.Prediction]) {
        if self.cluppies.count == 0 { //初回 or クリア後
            for i in 0..<boundingBoxes.count {
                if i < predictions.count {
                    let prediction = predictions[i]
                    if (prediction.classIndex == 0) { //クラッピーを検知
                        cluppies.append(self.createPlayerFromPrediction(from: prediction))
                    }
                }
            }
        } else { //2回目以降 : 確認処理
            var flgFire = false
            var targetCluppy: Player!
            var findIndexes: [Bool] = []
            for _ in cluppies {
                findIndexes.append(false)
            }
            // クラッピー(0, 1) のみフィルターして Playerに変換
            let currentCluppies = predictions.filter { $0.classIndex == 0 || $0.classIndex == 1 }.map { self.createPlayerFromPrediction(from: $0) } //クラッピーを検知

            // 同一オブジェクト判定 & 移動判定
            for cru in currentCluppies {

                // TODO : n回分で平均取る

                var isMatch = false
                for i in 0..<self.cluppies.count {
                    let old = self.cluppies[i]
                    if findIndexes[i] {
                        print("すでに発見ずみ \(i)")
                        continue //すでに発見済み
                        // FIXME: 認識率悪かったら取る
                    }

                    // 既存のエントリーと同じものか確認 (含まれているか, 重なりがあるか)
                    if old.rect.contains(cru.rect) || old.rect.intersects(cru.rect) {
                        if self.getCenterDiff(p1: old, p2: cru) { // 座標の変更量が閾値を超えたら動いたと判断
                            print("target is detected! - 1")
                            flgFire = true
                            targetCluppy = cru
                        } // else : 動きなし

                        findIndexes[i] = true
                        self.cluppies[i].rect = cru.rect //ジリジリ動いたときに検出できない場合は外す
                        isMatch = true
                        break
                    }
                }
                if !isMatch { //見つからなかった = 新規
                    self.cluppies.append(cru)
                }
                if flgFire { //見つかった&動いてる
                    break
                }
            }
            if flgFire { // ダルマさんが転んだ
                print("target is detected! - 2")
                self.currentMode = .terminator //クラッピー追跡モード

                // XYでどの領域に居るか判断
                // TODO : 動かして微妙だったら LEFT, CNTER, RIGHT (or もっと)に分ける
                let width = CGFloat(YOLO.inputWidth)
                let height = CGFloat(YOLO.inputHeight)
                let center = CGPoint(x: width / 2, y: height / 2)

                // 最初の追跡動作用のBLEコマンド
                var commandQueue : [BLECommand] = []
                commandQueue.append(BLECommand(kind: CommandKind.servomotorOn, time:0)) //クラッピーを起こす
                if targetCluppy.center.x < center.x { // 右
                    commandQueue.append(BLECommand(kind: CommandKind.turnLeft, time:500))
                } else if center.x < targetCluppy.center.x { //左
                    commandQueue.append(BLECommand(kind: CommandKind.turnRight, time:500))
                }
                commandQueue.append(BLECommand(kind: CommandKind.forward, time:2500)) //前進して迫る

                // BLE コマンド送信 開始
                self.cmdQueueItrt = CommandQueueIterator(commandQueue)
                self.setNextCommand()
            }
        }
    }

        /* 〜 以下略 〜 */
}

なお、TinyYOLOの認識では認識結果のサイズが前後のフレームで大きくブレる事があるため、移動量の認識には認識結果の中心点の座標を元に判定をしました。

内容自体は非常に単純ですが、iPhoneXのA11のパワーで推論を走らせることで、20FPS前後で安定してTinyYOLOの画像認識を走らせる事ができたことにより、 フレーム間の比較による物体の擬似的な追跡が可能になりました。

 

余談ですが、 Vision framework も、CoreBluetooth も、何かと UIViewController 上でないと動かない(動かしずらい)部分が多いのでちゃんと構造化しようとすると面倒ですが、そこはSwiftのメリットを生かして、 extension でうまいこと用途別に swiftファイルを分ける事でなんとか可読性を維持しました。

こういう点の勝手の良さはハッカソン向きだと思いました。
特に、ハッカソンの場合は きれいに実装できるか よりも 短時間にローコストで如何に確実に動かすか が大事なので、OSの縛りが強いこの辺りの機能の実装で可読性を保持するにはほんと役にたちます。

成果発表と完成品について

ステージ発表での様子はこちら。

我々のチームは49:50あたりから。

ステージ発表では残念ながらデモが上手くいかず。

原因としては多分こんな所かなぁ。

  • ESP32に使っていた電源&USBケーブルが若干不安定。(実際、ステージに上がってからリセット掛かって、慌てて再起動した)
  • ステージは暗い&特定の方向から強い光が当たっているので、iPhoneのカメラ&画像認識的には逆光になったり暗かったりで最悪のコンディションだった。

前者については、発表終わってから電源交換したり、ケーブル交換したら良くなったのであたり。
後者は実確認できてないので想像でしかないけど、後から思い返したら似たようなことあったので多分正解。

発表後は開発エリアに戻り、作品展示を行いました。
この時間は一般の方もいらっしゃいます。

作品展示中に頂いたコメント:

  • OpenCVですか?パターン認識ですか?と聞かれる事が多かった。
  • 使用技術が本気過ぎると言われた

フリック!ニュースさんからも

テクノロジーの無駄遣い感がすごいですが、昨夜の時点で、ちゃんとクラッピーを画像認識してました。

と、お褒めの言葉を頂きました。
秋葉原で、Yahoo! HACKDAY、本日開催です! #hackdayjp

総勢70オーバーのチームが参加していましたが、全体的に DeepLearning を使っているチームが結構いました。
(見てる感じですと、クラウドのAPIを使っているチームが多かったように思います。)


さて、AIQでは、私達と東京か札幌で一緒に働ける仲間を募集しています。
詳しくはこちら

私達と一緒にを様々な業界の未来を変えていきませんか?