[iOS]画像解析フレームワークVision FrameworkとUIKitの座標計算

前回、[iOS]画像解析フレームワークVision Framework入門にて、Vision Frameworkの概要を解説する記事を書きました。
そこで紹介したAppleのサンプルコード、Detecting Objects in Still Images | Apple Developer Documentationで私がつまづいた点をまとめます。
具体的には、Vision FrameworkとUIKitの座標空間が異なりその変換方法の理解を深めたいと思います。

Vision FrameworkとUIKitの座標空間

Vision Frameworkの座標空間は左下原点です。
一方UIKitの座標空間は左上原点から始まります。
なのでVision Frameworkから返った分析結果の矩形をUIImageViewに貼り付ける場合はVisionの座標空間からUIKitの座標空間に変換する必要があります。

図のようにUIKitは原点が左上でY軸が大きくなるに従って下方向に座標が動きます。
Visionは原点が左下でY軸が大きくなるに従って上方向に座標が動きます。

Vision Frameworkの結果矩形を反映する

Detecting Objects in Still Images | Apple Developer DocumentationではVisionが返した分析結果インスタンスVNRectangleObservationの矩形情報からレイヤーを作りViewに貼り付ける実装がされています。

まずdraw(rectangles:,onImageWithBounds:)メソッドが呼ばれます。

fileprivate func draw(rectangles: [VNRectangleObservation], onImageWithBounds bounds: CGRect) {
    CATransaction.begin()
    for observation in rectangles {
        let rectBox = boundingBox(forRegionOfInterest: observation.boundingBox, withinImageBounds: bounds)
        let rectLayer = shapeLayer(color: .blue, frame: rectBox)

        // Add to pathLayer on top of image.
        pathLayer?.addSublayer(rectLayer)
    }
    CATransaction.commit()
}

そしてboundingBox(forRegionOfInterest:,withinImageBounds:)にVisionの分析結果の矩形情報であるVNRectangleObservation.boundingBoxを渡してUIImageViewに貼り付けるレイヤー矩形を作成します。

boundingBox(forRegionOfInterest:,withinImageBounds:)の実装がこちら。

fileprivate func boundingBox(forRegionOfInterest: CGRect, withinImageBounds bounds: CGRect) -> CGRect {

    let imageWidth = bounds.width
    let imageHeight = bounds.height

    // Begin with input rect.
    var rect = forRegionOfInterest

    // Reposition origin.
    rect.origin.x *= imageWidth
    rect.origin.x += bounds.origin.x
    rect.origin.y = (1 - rect.origin.y) * imageHeight + bounds.origin.y

    // Rescale normalized coordinates.
    rect.size.width *= imageWidth
    rect.size.height *= imageHeight

    return rect
}

まずforRegionOfInterestは先程言ったとおりVNRectangleObservation.boundingBoxの値が渡されています。VNRectangleObservation.boundingBoxの値はVisionを取り込んだ画像に対する倍率の数値として返ってきます。
例えばこのような値です。

forRegionOfInterest	CGRect	(origin = (x = 0.62527656555175781, y = 0.72147649526596069), size = (width = 0.077916398644447327, height = 0.11678335070610046))	

倍率なので第二引数で添付する画像矩形のwithinImageBoundsの矩形情報をそれぞれかけ合わせればVisionが示した矩形情報をUIImageViewに乗せることができます。

X座標は画像の幅をかけて、画像のX座標分を足すことで調整しています。

rect.origin.x *= imageWidth
rect.origin.x += bounds.origin.x

WidthとHeightはそれぞれ画像の高さをかけ合わせることで調整しています。

// Rescale normalized coordinates.
rect.size.width *= imageWidth
rect.size.height *= imageHeight

ここまでは大丈夫。問題はY座標の調整です。

rect.origin.y = (1 - rect.origin.y) * imageHeight + bounds.origin.y

見たところ、X座標と同じように画像の高さ分かけて画像のY座標分を足すことで調整しているようですが、(1 - rect.origin.y)と1からy座標の値を引いています。
この理由が分からなかったのですが、Twitterでつぶやくと@sakiyamaKさんと@kenmazさんが答えてくれました。

rect.origin.yは0.0から1.0までの値をとって、左下座標空間から左上座標空間に移動する場合、Y軸が1の値上に移動すると考えればいいみたいですね。

Appleの公式ドキュメントCore Animation Basicsをみてみる

UIKitへの座標変換はVisionに限らず古くからあるものなので、Appleの公式ドキュメントに記述がないだろうかと探していたら、まさに同じような考え方がCore Animation Basicsにかかれていました。

Anchor Points Affect Geometric ManipulationsFigure 1-5です。図を抜粋してみます。

こちらでは同じ矩形情報でもiOSの座標空間とOS X(現macOS)の座標空間で表現される内容の違いが示されています。
OS Xでbounds = (40, 60, 120, 80)の矩形のY座標がiOSではどこになるかといえば、

200 - 60 = 140

で140になります。

まとめ

Visionの分析結果の座標をUIKitに変換する過程で(1 - rect.origin.y)の計算をする謎を解説しました。
座標空間の変換、慣れていないのでつまづいたら公式ドキュメントに立ち戻っていきたいと思います。

最後に、私の疑問に答えていただいた@sakiyamaKさんと@kenmazさんありがとうございました。

参考

宣伝

インプレスR&D社より、「1人でアプリを作る人を支えるSwiftUI開発レシピ」発売中です。
「SwiftUIでアプリを作る!」をコンセプトにSwiftUI自体の解説とそれを組み合わせた豊富なサンプルアプリでどんな風にアプリ実装すればいいかが理解できる本となっています。
iOS 14対応、Widgetの作成も一章まるまるハンズオンで解説しています。
SwiftUIを学びたい方、ぜひこちらのリンクをチェックしてください!




https://nextpublishing.jp/book/12491.html