カメラで撮るだけで物の大きさがわかる
便利だと思いませんか?
(仕組み)AIで物体検出してARで大きさを測る
物体検出モデル(Yolov8)でカメラに映った物体を検出し、その結果をもとにARセンサーで距離や物体の大きさを測れます。
方法
Yolov8を実行する
ARSessionのカメラフィードをYolov8にかけます。
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の検出結果を投影します。
デバイスと物体の距離を測る
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)
物体検出結果を物体の距離の平面に投影して幅と高さを測る
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)