2017.10.06

ARKit で野球のグラウンド作りは楽になるのか?

こんにちは。F.S. です。野球ネタ第2段です。

※第一弾はこれ→「iPhoneカメラ&画像認識で球速計測(いわゆるスピードガン)が実現できるか?


iOS11とともに、ARKitも正式リリースとなりました。
今回はARKitを使用して、少年野球コーチのお仕事では欠かせないグラウンドのライン(白線)引きに役立つものを作ってみたいと思います。

イメージとしてはこのようなものです。
  • グラウンドにホームベースを置いてスマホで映すと、引くべきラインのガイドが表示される
グラウンドは広い平面なので、そこそこ実測とずれるだろうなと思いつつ、許容範囲に収まるといいなーという淡い期待を抱いてスタートします。
AirMeasureで14mを計測したら60〜70cmくらいずれていたのは経験済みですが。。)

実装

実装はこんな流れで進めていきます。

1. ラインのジオメトリを準備
2. ARKitで平面を検出
3. OpenCVでホームベースの頂点位置を検出
4. 2の平面、3の頂点位置をもとにラインを描画

1. ラインのジオメトリを準備

まずはSceneKitにて適当なプロジェクトを作り、野球のラインに沿ったジオメトリ生成するクラスを作成しておきます。

SceneKitでのレンダリング結果はこんな感じになります。

※こちらのイメージはシミュレーター上でのものです

ピッチャープレートと1〜3塁が2つずつあるのは、少年野球は高学年と低学年で塁間の距離が異なるためです。
なお、ARKitのSceneView(ARSCNView)ではメートル単位の実空間座標に投影されるので、「16m幅ならwidth:16.0」とできるのが嬉しいですね。

2. ARKitで平面検出

ARKitプロジェクトを作成し、平面検出します。
この辺はチュートリアルやいろんなサンプルで実装が紹介されてますね。

一度検出した平面についても、検出を続けていると逐次情報更新されていき、より精度が高くなると言われています。

3. OpenCVでホームベースの頂点位置を検出

ARFrameのcapturedImageを取得し、OpenCVを使ってホームベースの頂点を検出します。
今回の実装では、画面をタップしたタイミングでホームベースの検出処理が始まるようにしています。
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

        // ARFrameからキャプチャ画像を取得
        guard let frame = sceneView.session.currentFrame else {
            print("No captured image.")
            return
        }

        // キャプチャ画像はCVPixcelBufferなので、OpenCVで扱えるUIImageに変換
        let ciImage = CIImage(cvImageBuffer: frame.capturedImage)
        let context = CIContext(options: nil)
        let cgImage = context.createCGImage(ciImage, from: ciImage.extent)
        let capturedImage = UIImage(cgImage: cgImage!)
        
        // ベースを認識。認識できなければ再度セッションを開始
        guard let baseVertices = BaseDetector.detect(capturedImage) as? [[NSInteger]] else {
            print("No homebase in the captured image.")
            return
        }
デバッガで確認する限り、ARSCNViewがPortraitの場合のキャプチャ画像は90°回転したものが取得されます。どこかにOrientation情報を持っているのではないかとは思うのですが、ドキュメントを漁っても見つかりませんでした。
ひとまず、ARSCNViewのフレームからPortraitかLandscapeかを判断することで逃げました。。

OpenCVによる頂点検出は次のようなコード例になります。
#import <opencv2/opencv.hpp>
#import "opencv2/imgcodecs/ios.h"
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "BaseballField-Bridging-Header.h"

@implementation BaseDetector: NSObject

+(NSArray *)detect:(UIImage *)image
{
    cv::Mat mat;
    UIImageToMat(image, mat);

    // 画像を2値化してエッジを際立たせる
    cv::Mat gray;
    cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);
    cv::threshold(gray, gray, 0, 255, cv::THRESH_OTSU);

    // エッジ検出器を使う
    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::findContours(gray, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_TC89_L1);

    NSArray *baseVertices = nil;
    double lastArea = 1000;    // 最低これ以上の面積を持つものだけを処理する
    
    // ホームベースの頂点候補を探す
    for(int i = 0; i < contours.size(); i++) {
        // エッジで囲まれた部分の面積が最大のものを採用する
        double area = contourArea(contours[i],false);
        if(area > lastArea) {

            //輪郭を直線近似
            std::vector<cv::Point> approx;
            cv::approxPolyDP(cv::Mat(contours[i]), approx, 0.01 * cv::arcLength(contours[i], true), true);

            // 五角形のみ取得
            if (approx.size() == 5) {
                NSMutableArray *baseVertexCandidate = [NSMutableArray array];
                for (int j = 0; j < approx.size(); j++){
                    baseVertexCandidates[j] = @[
                                               [NSNumber numberWithInteger:approx[j].x],
                                               [NSNumber numberWithInteger:approx[j].y]
                                               ];
                }
                baseVertices = baseVertexCandidates;
            }
            lastArea = area;
        }
    }
    
    return baseVertices;
}
findContoursで検出されるエッジはこのようなイメージです。
上記はかなりうまくできている例ですが、通常はノイズを含んだ複数の領域が検出されます。

4. ラインを描画

まずは上記3で検出したホームベースの2次元頂点座標を、2で検出した平面に対して当たり判定(HitTest)し、3次元ワールド座標に変換します。

ベースの頂点で実際に必要になるのは、1のジオメトリの原点でもあるキャッチャー側の先端部分と、ピッチャー側の両端の合わせて3点です。ちょっと表現が難しいので、下の図を参考ください。

これに合わせて1のジオメトリもつノードをARSCNViewに追加すると、このようにベースに合わせてラインが描画されます。

ちょっと傾けてみた感じ。うーん、微妙にずれてきますねぇ。

やってる途中でVuforiaとかでマーカー作った方が簡単にできるんじゃない?? と思いつつ、、、ひとまずそれらしい形にはなりました。

実際のグラウンドにて・・・

さて、実際にいつも練習している小学校の校庭で試してみました。
デバイスはiPhone7です。

まずは7〜8秒くらいかけて平面を検出し、ホームベースを認識します。これだけ見るとバッチリですね。

デバイスを傾けて一塁方向に向けてみます。微妙にずれています。

ゆっくりと一塁ベースに向かって歩いて行き、

低学年の一塁ベース位置にたどり着きました。写真の中央下側、ラインに重なる位置に見えているマークが低学年の一塁ベースである21mの位置です。ベース3個分くらい(およそ1.2m)ずれています。

そして、ホームベース位置に戻ってくると、こんなにずれてしまいました。。

おわりに

想定はしていましたが、結構ずれてしまいますね。
距離的にはだいたい5%程度のずれがある感覚です。

実際にラインを引くには厳しいですが、一三塁方向の目安程度にはなるのかなぁと。
やはり測量的なことをやろうとすると、Tangoレベルでないと難しいですね。

それでは、また。

次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

Pocket

関連記事