見出し画像

【RaycastでYOLOで見つけたボールまでの距離を測る】:「視覚」サポートテクノロジー試行シリーズ

週刊プレイグラウンズ第45号
iPad Swift Playgroundで「見る」をつくる

はじめに

前回の44号では、YOLOが見つけたボールの位置をARKitの空間へ渡し、その場所に仮想の球を配置しました。
一度位置を決めたあとはYOLOによる認識を停止しても、デバイスを動かすと、仮想球は現実のボールと同じ場所に残ります。
つまり、
YOLOが見つける → ARKitが位置を覚える
ところまで進みました。
今回は、その位置までの距離を測って読み上げます。

前回の記事


Raycastで距離を測る

今回使うのが Raycast です。
ARKitのRaycastでは、画面上のある位置から現実空間へ向かって仮想的な線を伸ばし、ARKitが認識している平面などとの交点を調べることができます。
今回のコードでは、YOLOが認識したボールの矩形の下辺中央を使います。
ボールそのものの中心ではなく、床と接していると考えられる位置です。
流れを簡単にすると、
YOLOでボールを認識
 

ボールの下辺中央を画面上の位置に変換
 

そこから床へRaycast
 

床上の3D位置を取得
となります。コード(記事の最後)では次の部分です。

var results =
    arView.raycast(
        from: screenPoint,
        allowing: .existingPlaneGeometry,
        alignment: .horizontal
    )

まず、ARKitが実際に認識している水平面との交点を探します。ただし、YOLOが求めた位置が認識済みの床平面の境界から少し外れる場合もあります。そのため、見つからなかった場合には、

arView.raycast(
    from: screenPoint,
    allowing: .existingPlaneInfinite,
    alignment: .horizontal
)

として、認識した水平面を延長してもう一度調べています。
このあたりも実際に使いながら調整していく部分です。


ボールの位置を3D空間に保存する

Raycastで得られるのは床面上の位置です。
ボッチャのボールは直径86mmなので、今回のコードでは半径を43mmとして、その位置から43mm上をボールの中心としています。

let ballCenter =
    floorPosition +
    SIMD3<Float>(
        0,
        bocciaBallRadius,
        0
    )

そして、この位置を保存します。

ballWorldPosition = ballCenter

ここが今回の重要なポイントです。
保存しているのは画面上の座標ではありません。
現実空間の3D座標です。
そのため、ボールを見つけたあとにiPadを動かしても、ARKit上ではボールの位置が変わりません。
44号で配置した仮想球も、この位置に置かれています。


最初は距離だけ読み上げる

以前、YOLOだけを使ってボッチャ球を探した時には、ボールを見つけるまでカメラを動かしていました。つまり、認識した瞬間には、対象のボールはおおむねカメラの正面にあります。
そこで今回も、最初にボールを見つけた時には方向を読み上げません。
たとえば、
「1メートル40センチ」
と距離だけを読み上げます。
「右0度、1メートル40センチ」
などと読み上げても、その時点ではあまり意味がないでしょう。


距離は10cm単位

今回、距離は10cm単位に丸めています。

let centimeters =
    Double(distance) * 100.0

return
    Int(
        (
            centimeters /
            10.0
        ).rounded()
    ) * 10

ARKitからは、もっと細かな値を取得できます。
ただしYOLOで推定した位置ですので、誤差は少なくないでしょう。
そして今回作ろうとしているのは測量器ではありません。
視覚に障害のある人がボッチャのボールのおおよその位置関係を知るための試みです。
1m30cm
1m40cm
そこで、10cm単位に丸めることにしました。


もう一度確認するときは方向も必要

ボールを見つけたあと、利用者がiPadを動かすことがあります。
その状態でもう一度距離を確認すると、最初とは状況が違います。
ボールは必ずしも正面にはありません。
そこで、画面をタップするか「位置を読み上げ」ボタンを押すと、
「正面、1メートル40センチ」
「右20度、1メートル40センチ」
「左10度、1メートル40センチ」
のように読み上げるようにしました。
方向も細かくしすぎず、10度単位です。


Raycastをもう一度する必要はない

再読み上げのたびにRaycastをしているわけではありません。
最初に取得したボールの3D位置は、

ballWorldPosition

として保存されています。
一方、ARKitからは現在のカメラの位置と向きを取得できます。
そこで、
保存されているボールの位置

現在のカメラの位置・向き
を比較します。
これによって、その時点でボールが正面なのか、右なのか、左なのかを計算できます。
世界座標のボール位置を現在のカメラ座標へ変換して、

let radians =
    atan2(
        cameraPoint.x,
        -cameraPoint.z
    )

から左右の角度を求めています。
つまり、一度場所がわかれば、その後はYOLOでボールを探し続ける必要も、Raycastを繰り返す必要もありません。
物体を見つけるのはYOLO。
場所を保持するのはARKit。
という役割分担です。


距離は床面上で考える

今回のコードでは、カメラからボールまでの直線距離ではなく、床面上での距離を求めています。

let dx =
    ballPosition.x -
    cameraPosition.x

let dz =
    ballPosition.z -
    cameraPosition.z

return sqrt(
    dx * dx +
    dz * dz
)

カメラには床からの高さがあります。その高さまで含めた斜めの距離よりも、ボッチャでは床面上でどのくらい離れているかの方がわかりやすいと考えました。つまりプレイヤーの脚元からボールまでの距離です。
これも目的によって変わります。
別の用途なら3D空間での直線距離を使ってもよいかもしれません。


ここから先はいろいろ試せそうです

今回のコードは、ボッチャを題材にしています。
しかし、
物体を認識して、その現実空間での位置を取得する
ところまでできれば、いろいろな試行が考えられます。
たとえば、

  • 複数の対象までの距離を比較する

  • 距離に応じて読み上げ方を変える

  • 音声ではなく振動で方向を伝える

  • 一度見つけた対象の場所をあとから再確認する

なども考えられそうです。今回のコードをベースに、それぞれの目的に合わせて試してみるのも面白いと思います。


まだ「どのボールか」はわからない

ここまでで、
YOLOでボールを見つける
ARKitで位置を固定する
Raycastで距離を求める
あとから方向と距離を確認する
ところまで進みました。
しかし、ボッチャで使うにはまだ大きな問題があります。ボッチャでは、白い的球と、赤・青のゲームボールを区別する必要があります。
これまでの試行では、色を画像から判断することも試しました。
ところが実際の場所で試してみると、照明の影響をかなり受けました。
特に、照明の色温度などによって、白いボールが赤系の色として判定されてしまうことがありました。
画面上では明らかに白く見えていても、カメラから取得した色の値が、想定した「白」になるとは限りません。


次は、その場所で色を覚えさせる

そこで次回は、固定した色の基準だけで判定するのではなく、
ゲームを始める前に、その場所、その照明、そのボールで色を確認する
方法を試します。
実際に使用する、
・白
・赤
・青
の3色のボールをカメラで撮影し、その時の色を基準として保存します。
そしてゲーム中には、その基準に近い色かどうかを調べます。
いわば、ゲームを始める前の色キャリブレーションです。
実際の環境でうまく動かなかったことから、次の方法を考える。
生成AIを使えばコードを書く速度は大きく上げられますが、このような部分は実際に試してみなければわかりません。
次回は、白・赤・青のボールを実際の環境に合わせて識別する方法を試します。


今回のコード

iPad Swift Playground、iOS 18以降、実機で動作します。
アプリ設定の「機能」から、カメラの利用許可を追加してください。
また、YOLOモデルとしてyoloV3.mlmodelをプロジェクトへ追加します。

次の号を参考に、

ContentView.swiftを更新すると良いでしょう。


おわりに ── 応援について

このシリーズの記事とコードは、すべて無料で公開しています。これからも、そのつもりです。視覚サポートのための技術が、少しでも多くの人の手に届くように ── それがこの連載を続けている理由だからです。

そのうえで、もし記事が役に立った、続きを読んでみたいと感じてもらえたら、note のチップ(応援)で背中を押していただけると、とても励みになります。いただいた応援は、実機でのテストや、次の号の試行錯誤を続けていくための力になります。

もちろん、読んでいただくこと自体が、いちばんの応援です。ここまで目を通してくださって、ありがとうございました。次号でまたお会いしましょう。

いいなと思ったら応援しよう!