[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さんが答えてくれました。
うーん。Visionの勉強にとAppleのサンプルコードを読んでいるけどわからないヶ所がある。
— SatoTakeshi 【BOOTH SwiftUI開発レシピ】 (@hatakenokakashi) May 10, 2020
Vision結果からもとの画像の矩形にマッピングするところで(1-rect.origin.y)してるのはなんでだろう?
L440
rect.origin.y = (1 - rect.origin.y) * imageHeight + bounds.origin.yhttps://t.co/UbJw8WNFJJ
単純にこういうことかと思います。左下原点を左上原点に変換したいだけ pic.twitter.com/vuiFJjaPTm
— kenmaz (@kenmaz) May 11, 2020
rect.origin.y
は0.0から1.0までの値をとって、左下座標空間から左上座標空間に移動する場合、Y軸が1の値上に移動すると考えればいいみたいですね。
Appleの公式ドキュメントCore Animation Basicsをみてみる
UIKitへの座標変換はVisionに限らず古くからあるものなので、Appleの公式ドキュメントに記述がないだろうかと探していたら、まさに同じような考え方がCore Animation Basicsにかかれていました。
Anchor Points Affect Geometric ManipulationsのFigure 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を学びたい方、ぜひこちらのリンクをチェックしてください!