検出した矩形を自由にトリミングする

## はじめに
SwiftUIでiOSアプリ作成している中で画像内の矩形を自由にトリミングしたいと思って、作っていた機能を詳しく解説していこうかと思います。
少しでも役に立てていただければなと思っています。

本記事では、SwiftUIにて作成したアプリで撮影した画像に対して、
VisionFrameworkで矩形検出を行い、検出結果を描画し、画面上で任意に大きさを変えることができるような機能になります。
今回は、自家製のカメラの作成や写真の取り込み方法は割愛します。

### 実装
さっそく、実装の方に見ていきます。

#### 矩形検出リクエスト
まずは取り込んだ画像に対して、VisionFrameworkを使用して矩形検出していきます。
```swift
func detectRectangleInImage(_ image: UIImage) {
    guard let cgImage = image.cgImage else {
        return
    }

    // Visionリクエストの作成
    let request = VNDetectRectanglesRequest { request, error in
        if let results = request.results as? [VNRectangleObservation], let rectangle = results.first {
            // 取得後の処理（後続へ）
            }
        }
    }

    // リクエストを実行
    let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
    DispatchQueue.global(qos: .userInitiated).async {
        try? handler.perform([request])
    }
}
```
VNDetectRectanglesRequestを作成して、画像に対して、矩形検出してもらいます。
この時作成したVNDetectRectanglesRequestに対して、オプションを作成することができます。

```swift
    // リクエストのパラメータを調整
    request.minimumAspectRatio = 60 / 85
    request.maximumAspectRatio = 70 / 95
```
こちらの記事を参考にさせていただきました。
https://qiita.com/sy-hash/items/a21a825acbc69b78983a

今回は主に検出する矩形の比率を指定しました。
決まった大きさの矩形だとこのように最小値と最大値を設定しておくことで、決まった矩形のみ検出します。
あとは、バックグラウンドスレッドにてリクエスト実行します。

#### 矩形検出結果
作成したVNDetectRectanglesRequestのクロージャの中でレスポンスの処理を行なっていきます。
```swift
// Visionリクエストの作成
let request = VNDetectRectanglesRequest { request, error in
    if let results = request.results as? [VNRectangleObservation], let rectangle = results.first {
        DispatchQueue.main.async {
            // ① 画像サイズに合わせた座標
            let rect = convertBoundingBox(rectangle, imageSize: self.imageSize)
            // ② Rectangleの座標が中心になるので、サイズ分ずらす
            viewPosition = CGPoint(x: rect.origin.x + rect.width / 2,
                                   y: rect.origin.y + rect.height / 2)
            viewSize = CGSize(width: rect.width, height: rect.height)
        }
    }
}
```

##### Point① 画像に合わせた座標
この時結果で取得できたbboxの値はUIKitの座標と異なるので、変換を行う
UIKitの原点は左上、Vision Frameworkの原点は左下となる。
そのため、y座標を反転して、座標を取得
```swift
func convertBoundingBox(_ boundingBox: CGRect, imageSize: CGSize) -> CGRect {
    let width = boundingBox.width * imageSize.width
    let height = boundingBox.height * imageSize.height
    let x = boundingBox.minX * imageSize.width
    let y = (1 - boundingBox.maxY) * imageSize.height // Y座標を反転

    return CGRect(x: x, y: y, width: width, height: height)
}
```
self.imageSizeについては、View側で値を取得する
self.imageSizeの取得例
```swift
Image(uiImage: photoImage)
    .resizable()
    .scaledToFit()
    .background(
        GeometryReader { geometry in
            Color.clear.onAppear {
                // 表示時1回目で画像のサイズを取得
                self.imageSize = geometry.size
            }
        }
    )
```

##### Point② Rectangleの座標が中心になるので、サイズ分ずらす
Rectangleで枠を囲むときRectangleの大きさの中心から座標が決まるので、
この時widthとheightの半分ずらして、座標位置を決めています。
```swift
self.viewPosition = CGPoint(x: rect.origin.x + rect.width / 2, 
                            y: rect.origin.y + rect.height / 2)
self.viewSize = CGSize(width: rect.width, height: rect.height)
```

上記の結果で取得できたviewPositionとviewSizeを使って、Rectangleを描画する

```swift
Image(uiImage: photoImage)
    .resizable()
    .scaledToFit()
    .overlay(content: {
        Rectangle()
            .fill(.red) // 明示的に赤色で矩形を確認
            .frame(width: size.width, height: size.height)
            .position(position)
    })
    .background(
        GeometryReader { geometry in
            Color.clear.onAppear {
                imageSize = geometry.size
            }
        }
    )
    .task {
        // 撮影したUIImageを引数として渡す
        detectCardInImage(photoImage)
    }
```

このあとは表示した矩形を画面上で自由に動かせる処理を記載していきますが、
それはまた次の記事になります。
更新をお待ちください。


## 参考
https://developer.apple.com/documentation/avfoundation/avcam-building-a-camera-app
https://qiita.com/sy-hash/items/a21a825acbc69b78983a

https://blog.personal-factory.com/2020/05/17/convert-uikit-coordinate-from-vision/