iPhoneカメラで物体認識・大きさを自動計測する

# カメラで撮るだけで物の大きさがわかる

![Jun-20-2023 13-20-24.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/71bbea94-a134-2d82-3efb-df8f548d5287.gif)

便利だと思いませんか？

# （仕組み）AIで物体検出してARで大きさを測る
物体検出モデル（Yolov8）でカメラに映った物体を検出し、その結果をもとにARセンサーで距離や物体の大きさを測れます。
![名称未設定2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235259/e0302c4f-26df-9882-29df-ca11afa19101.jpeg)


# 方法
### Yolov8を実行する

ARSessionのカメラフィードをYolov8にかけます。

```swift
lazy var yoloRequest: VNCoreMLRequest = {
    do {
        let model = try yolov8s().model
        let vnModel = try VNCoreMLModel(for: model)
        self.yoloRequest = VNCoreMLRequest(model: vnModel)
        self.yoloRequest.imageCropAndScaleOption = .scaleFit
    } catch let error {
        fatalError("mlmodel error.")
    }
}()


func session(_ session: ARSession, didUpdate frame: ARFrame) {
    let pixelBuffer = frame.capturedImage
    var ciImage = CIImage(cvPixelBuffer: pixelBuffer, options: [:])
    ciImage = ciImage.oriented(.right)
    let aspect =  sceneView.bounds.width / sceneView.bounds.height
    let estimateWidth = ciImage.extent.height * aspect          
    cropped = ciImage.cropped(to: CGRect(
        x: ciImage.extent.width / 2 - estimateWidth / 2,
        y: 0,
        width: estimateWidth,
        height: ciImage.extent.height
    )
    let handler = VNImageRequestHandler(ciImage: cropped, options: [:])
    do {
        try handler.perform([yoloRequest])
        guard let result = yoloRequest.results?.first as? VNRecognizedObjectObservation else { return }
        let topLeft = CGPoint(x: result.boundingBox.minX, y: 1 - result.boundingBox.maxY)
        let topRight = CGPoint(x: result.boundingBox.maxX, y: 1 - result.boundingBox.maxY)
        let bottomLeft = CGPoint(x: result.boundingBox.minX, y: 1 - result.boundingBox.minY)
        let bottomRight = CGPoint(x: result.boundingBox.maxX, y: 1 - result.boundingBox.minY)    
        let deNormalizedTopLeft = VNImagePointForNormalizedPoint(topLeft, Int(frameRect!.width), Int(frameRect!.height))
        let deNormalizedTopRight = VNImagePointForNormalizedPoint(topRight, Int(frameRect!.width), Int(frameRect!.height))
        let deNormalizedBottomLeft = VNImagePointForNormalizedPoint(bottomLeft, Int(frameRect!.width), Int(frameRect!.height))
        let deNormalizedBottomRight = VNImagePointForNormalizedPoint(bottomRight, Int(frameRect!.width), Int(frameRect!.height))

    } catch let error {
        print(error)
    }
}
```

iOS用に変換されたYolov8を以下からダウンロードし、xcodeプロジェクトにバンドルします。
https://github.com/john-rocky/CoreML-Models#yolov8

カスタムモデルを変換したい場合は以下の手順で変換できます。
https://qiita.com/john-rocky/items/0817bbd16667df183807

カメラフレームをクロップしています。
ARでディスプレイ上の物体位置を3次元空間に投影して大きさを測るので、
「ディスプレイに写っている映像」と「物体検出にかける画像」の領域を一致させるためです。
カメラのフレーム全体がディスプレイに写っているわけではないので。

それから、VNCoreMLRequestはデフォルトで前処理で画像を中央クロップするので、imageCropAndScaleOptionは.scaleFitにして、入力画像全体を物体検出にかけられるようにします。

### 検出したボックスを3次元に投影して距離を計測する

ディスプレイと対象物との距離を測り、その距離でYoloの検出結果を投影します。

### デバイスと物体の距離を測る

```swift
let raycastQuery = sceneView.raycastQuery(from: cgPoint, allowing: .estimatedPlane, alignment: .any)
if let unwrappedRaycastQuery = raycastQuery {
            let raycastResults = sceneView.session.raycast(unwrappedRaycastQuery)
            guard let result = raycastResults.first else { return nil }
            let worldCoordinates = simd_float3(
                x: result.worldTransform.columns.3.x,
                y: result.worldTransform.columns.3.y,
                z: result.worldTransform.columns.3.z
            )

let devicePosition = simd_float3(x: transform.x, y: transform.y, z: transform.z)
let distanceToSurface = distance(devicePosition,surfaceCenter)
````

### 物体検出結果を物体の距離の平面に投影して幅と高さを測る

```swift
let infrontOfCamera = SCNVector3(x: 0, y: 0, z: -distanceToSurface)
guard let cameraNode = sceneView.pointOfView else { return }
let pointInWorld = cameraNode.convertPosition(infrontOfCamera, to: nil)
var screenPos = sceneView.projectPoint(pointInWorld)
screenPos.x = Float(deNormalizedTopLeft.x)
screenPos.y = Float(deNormalizedTopLeft.y)
let leftTopCoordinate = simd_float3(x: (screenPos.x, y: screenPos.y, z: screenPos.z)

var screenPos = sceneView.projectPoint(pointInWorld)
screenPos.x = Float(deNormalizedTopRight.x)
screenPos.y = Float(deNormalizedTopRight.y)
let rightTopCoordinate = simd_float3(x: (screenPos.x, y: screenPos.y, z: screenPos.z)

let objectWidth = distance(leftTopCoordinate,rightTopCoordinate)
```