[iOS17]Visionで写真の前景をトリミング

# Visionで画像の前景をトリミングする
iOS17からVisionに画像の前景をトリミングするAPIが追加されました
写真アプリで長押しするとハイライトされるアレです！
写真アプリにはiOS16から入っていたのですが、iOS17からVisionにAPIが入ったので、アプリにこの機能を簡単に組み込めるようになりました

![Frame 288.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/219b9f77-2888-c405-6347-1fdda4844595.png)

WWDC23のセッション↓

https://developer.apple.com/videos/play/wwdc2023/10176/?time=165

# 前景を全て含むマスク画像を作成する
これだけのコードで前景のマスク画像を作成できます

```swift
func makeMask(inputImage: CIImage) -> CIImage {
    let request = VNGenerateForegroundInstanceMaskRequest()
    let handler = VNImageRequestHandler(ciImage: inputImage)
    try! handler.perform([request])
    let result: VNInstanceMaskObservation = request.results!.first!

    let mask = try! result.generateScaledMaskForImage(forInstances: result.allInstances, from: handler)
    return CIImage(cvPixelBuffer: mask)
}
```

こちらが実行結果です
出力されるCIImageのcolor spaceはkCGColorSpaceModelMonochromeで8bitのモノクロ画像のようです
前景っぽいところが255(白)で背景っぽいところが0(黒)となっているようです

![Frame 289.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/dd9ccc2f-4bb8-002f-7542-2ee157d1c85a.png)

# 複数の前景を含む場合に前景を選んでマスク画像を作成する
画像に複数の前景が含まれる場合、前景を選んでマスク画像を作成したいです
VNInstanceMaskObservation.instanceMaskに検出した前景ごとのラベルがついた画像が入っています
512x512の正方形の8bitのモノクロ画像で、背景の領域が0、前景のインスタンスの領域が1, 2, 3...と順番に入っていくようです
切り出したい領域にある前景のインスタンスのラベルを調べて、複数の前景を含む写真から指定した前景のみを含むマスクを作成できます

![Frame 290.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/b7955502-ce0b-26b6-957c-24f36d609473.png)

instanceMaskの指定した座標のラベルを調べて、generateScaledMaskForImageにそのラベルを渡します
instanceMaskが入力画像によらず正方形のようなので、入力された座標を正規化し、instanceMaskのサイズに合わせて座標を求めるようにします

```swift
func makeMask(inputImage: CIImage, atNormalizedSelectedPoint normalizedSelectedPoint: CGPoint) -> CIImage {
    let request = VNGenerateForegroundInstanceMaskRequest()
    let handler = VNImageRequestHandler(ciImage: inputImage)
    try! handler.perform([request])
    let result: VNInstanceMaskObservation = request.results!.first!

    let i = getInstanceLabel(atNormalizedPoint: normalizedSelectedPoint, instanceMask: result.instanceMask)
    let mask = try! result.generateScaledMaskForImage(forInstances: [Int(i)], from: handler)
    return CIImage(cvPixelBuffer: mask)
}

func getInstanceLabel(atNormalizedPoint normalizedPoint: CGPoint, instanceMask: CVPixelBuffer) -> UInt8 {
    let point = VNImagePointForNormalizedPoint(normalizedPoint,
                                               CVPixelBufferGetWidth(instanceMask) - 1,
                                               CVPixelBufferGetHeight(instanceMask) - 1)

    CVPixelBufferLockBaseAddress(instanceMask, .readOnly)
    defer {
        CVPixelBufferUnlockBaseAddress(instanceMask, .readOnly)
    }

    let pixels = CVPixelBufferGetBaseAddress(instanceMask)!
    let bytesPerRow = CVPixelBufferGetBytesPerRow(instanceMask)
    let instanceLabel = pixels.load(fromByteOffset: Int(point.y) * bytesPerRow + Int(point.x), as: UInt8.self)
    return instanceLabel
}
```
instanceLabelが2のエリアを指定して出力すると、2のラベルの前景のインスタンスのみのマスク画像が作成されます

![Frame 291.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214313/b822d93c-b86d-1b21-b813-fc6fec484d4c.png)


# まとめ
iOS17から新しく入ったVisionの画像の前景をトリミングするAPIを使ってみました
少ないコードで導入可能で、かなり精度も良いように感じました
色々な機能に活用することができそうです