iOSで画像内のオブジェクトを抽出する方法（VNGenerateForegroundInstanceMask）

Photosアプリでは、画像からオブジェクト（動物、人など）を抽出するために長押しすることができます。本記事では、この機能をImageAnalysisInteractionを使用して実装する方法について説明します。また、`VNGenerateForegroundInstanceMaskRequest`を呼び出す方法についても詳しく掘り下げます。

この記事はUIKitとSwiftUIの両方のアプリケーションに適用されます。
- 与えられた画像内のオブジェクトを検出する
- コード内で異なるオブジェクトをハイライトする
- オブジェクトの画像を取得する

この記事のボーナスとして、以下の方法も紹介します：
- タップした位置のオブジェクトを取得する
- 被写体の背後の画像背景を置き換える

:::note warn
注意：この記事内のコードはシミュレータでは動作しません。
物理デバイスを使用してテストしてください。
:::

:::note
SwiftUIのコードはUIKitのコードの後に続きます。
:::

さあ、始めましょう！

![IMG_2097E2EEAB6F-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/14d1d72f-ecde-9e9b-875f-b2ec8cbe326a.jpeg)

# 方法 1: UIImageViewに画像解析コンポーネントをアタッチ

## 画像内のオブジェクトを検出
画像解析を実行するには、`UIImageView` に `ImageAnalysisInteraction` を追加する必要があります。

```swift
private let imageView = UIImageView()
private let interaction = ImageAnalysisInteraction()

imageView.image = imageObject
imageView.contentMode = .scaleAspectFit
interaction.preferredInteractionTypes = .imageSubject
imageView.addInteraction(interaction)
```

:::note
`interaction` 変数には関数全体でアクセスする必要があるため、
これらをビューまたはビュー・モデルの変数として保存する必要があります。
:::

ここでは、好みのインタラクションタイプを設定できます。AppleのPhotosアプリを使用すると、画像内のオブジェクトだけでなく、テキストやQRコードも選択できることがわかります。これはpreferredInteractionTypesプロパティを使用して定義されます。このプロパティに配列を提供することで、ユーザーがアプリの画像ビューでどのオブジェクトと対話できるかを設定できます。

```swift
interaction.preferredInteractionTypes = [.dataDetectors, .imageSubject, .textSelection, .visualLookUp]
```

`.dataDetectors` は、URL、メールアドレス、物理アドレスを意味します。
`.imageSubject` は、画像内のオブジェクトを意味します（この記事の主な焦点）。
`.textSelection` は、画像内のテキストを選択することを意味します。
`.visualLookUp` は、iOSシステムが詳細情報を表示できるオブジェクトを意味します（例：猫や犬の品種）。
この記事では、これを `.imageSubject` のみに設定できます。

## 画像解析の実行
画像解析を実行して、画像内のどのオブジェクトがあるかを確認するには、以下のコードを実行します：

```swift
private let analyzer = ImageAnalyzer()
// run image analysis
Task { @MainActor in
    do {
        let configuration = ImageAnalyzer.Configuration([.visualLookUp])
        let analysis = try await analyzer.analyze(imageObject, configuration: configuration)
        interaction.analysis = analysis
        let detectedSubjects = await interaction.subjects
        // interaction.highlightedSubjects = detectedSubjects
    } catch {
        self.errorMessage = error.localizedDescription
    }
}
```

:::note
`analyzer` 変数には関数全体でアクセスする必要があるため、
これらをビューまたはビュー・モデルの変数として保存する必要があります。
:::

`interaction.highlightedSubjects` プロパティを使用して、検出された各オブジェクトまたはすべてのオブジェクトをハイライトすることもできます。上記のコードでは、この変数を `detectedSubjects` に設定すると、検出されたすべてのオブジェクトがハイライトされます。

## 画像解析からデータを読み取る

interaction.subjectsプロパティとinteraction.highlightedSubjectsプロパティを使用して、コード内でオブジェクトを読み取り、ハイライトするサブジェクトを設定できます。各サブジェクト（ImageAnalysisInteraction.Subjectに準拠）内で、サイズ、原点（バウンディングボックス）を読み取り、画像を抽出することができます。

![1*R4cIBzJqfm6ZsN3-_R1ngg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/0eb9c7e0-7866-2140-69d7-822f217f306a.png)

サイズとバウンディングボックスへのアクセス:

```swift
ForEach(self.detectedObjects) { object in
    Text("Position: x \(object.bounds.origin.x) y \(object.bounds.origin.y)")
    Text("Size: width \(object.bounds.width) height \(object.bounds.height)")
    Text("Object hash: \(object.hashValue)")
}
```

オブジェクトの切り抜かれた画像を取得するには、

```swift
Task { @MainActor in
    if let objectImage = try? await object.image {
        self.extractedObjectImage = objectImage
    }
}
```

オブジェクトを選択済み (highlight) としてマークするには：


```swift
interaction.highlightedSubjects.insert(object)
```

ハイライトリストからオブジェクトを削除するには：

```swift
interaction.highlightedSubjects.remove(object)
// interaction.highlightedSubjects = []
```

![1*JhIlM0y8G6lsX0c1Sl8uPA.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/5ed6505b-a53b-0d56-115d-88b1b4579b7e.jpeg)

## ハイライトされた（選択された）オブジェクトのための単一の画像を取得する
任意のオブジェクトを組み合わせて単一の画像を取得することもできます。

例えば、現在ハイライトされている左端の猫と右端の猫の画像を取得できます。

![1*HbltUA8F_ycummv6ccaVXg.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/c82f387c-fc97-bede-7f94-6e5dadd23601.jpeg)

以下のコードでは、すべてのハイライトされたオブジェクト（interaction.highlightedSubjects）を使用して `interaction.image` 関数を呼び出します：

```swift
func generateImageForAllSelectedObjects() async throws {
    let allSubjectsImage = try await interaction.image(for: interaction.highlightedSubjects)
    self.imageForAllSelectedObjects = allSubjectsImage
}
```

## SwiftUI互換ビュー

上記のロジックをSwiftUIで使用する場合、3つのファイルを書くことができます。

ここでは、SwiftUIビューImageAnalysisViewModelと互換ビューObjectPickableImageViewの間でデータを共有するのに役立つObservableObjectを示します。

ObservableObject:

```swift
@MainActor
class ImageAnalysisViewModel: NSObject, ObservableObject {
    let analyzer = ImageAnalyzer()
    let interaction = ImageAnalysisInteraction()
    
    func analyzeImage(_ image: UIImage) async throws -> Set<ImageAnalysisInteraction.Subject> {
        let configuration = ImageAnalyzer.Configuration([.visualLookUp])
        let analysis = try await analyzer.analyze(image, configuration: configuration)
        interaction.analysis = analysis
        let detectedSubjects = await interaction.subjects
        return detectedSubjects
    }
}
```

互換ビュー:

```swift
@MainActor
struct ObjectPickableImageView: UIViewRepresentable {
    
    var imageObject: UIImage
    
    @EnvironmentObject var viewModel: ImageAnalysisViewModel
    
    func makeUIView(context: Context) -> CustomizedUIImageView {
        let imageView = CustomizedUIImageView()
        imageView.image = imageObject
        imageView.contentMode = .scaleAspectFit
        viewModel.interaction.preferredInteractionTypes = [.imageSubject]
        imageView.addInteraction(viewModel.interaction)
        return imageView
    }
    
    func updateUIView(_ uiView: CustomizedUIImageView, context: Context) { }
    
}

class CustomizedUIImageView: UIImageView {
    override var intrinsicContentSize: CGSize {
        .zero
    }
}
```

SwiftUIビュー:

```swift
import SwiftUI
import PhotosUI
import VisionKit

struct ObjectExtraction: View {
    
    /* code related to image picking */
    @State private var userPickedImage: UIImage?
    @State private var userPickedImageItem: [PhotosPickerItem] = []
    
    /* image analysis result */
    @State private var detectedObjects: Set<ImageAnalysisInteraction.Subject> = []
    
    /* code related to image extraction */
    @StateObject private var viewModel = ImageAnalysisViewModel()
    @State private var extractedObjectImage: UIImage?
    @State private var imageForAllSelectedObjects: UIImage?
    
    /* code related to error reporting */
    @State private var errorMessage: String?
    
    var body: some View {
        
        ScrollView {
            VStack {
                
                /* image picker */
                PhotosPicker(
                    selection: $userPickedImageItem,
                    maxSelectionCount: 1,
                    matching: .images) {
                        Image(systemName: "photo")
                    }
                    .onChange(of: userPickedImageItem) { _, newValue in
                        Task { @MainActor in
                            do {
                                // load the image
                                guard let loadedImageData = try await newValue.first?.loadTransferable(type: Data.self),
                                      let loadedImage = UIImage(data: loadedImageData) else { return }
                                self.userPickedImage = loadedImage
                                // analyze this image
                                self.detectedObjects = try await self.viewModel.analyzeImage(loadedImage)
                            } catch {
                                self.errorMessage = error.localizedDescription
                            }
                        }
                    }
                /* */
                
                if let userPickedImage {
                    VStack {
                        Text("Image picked")
                            .font(.headline)
                        ObjectPickableImageView(imageObject: userPickedImage)
                            .scaledToFit()
                            .cornerRadius(20)
                            .frame(height: 300)
                            .environmentObject(viewModel)
                    }
                }
                
                HStack {
                    
                    if let extractedObjectImage {
                        VStack {
                            Text("Single object")
                                .font(.headline)
                            Image(uiImage: extractedObjectImage)
                                .resizable()
                                .scaledToFit()
                                .padding()
                                .background {
                                    RoundedRectangle(cornerRadius: 10)
                                        .foregroundStyle(.teal)
                                }
                                .frame(height: 300)
                        }
                    }
                    
                    if let imageForAllSelectedObjects {
                        VStack {
                            Text("All objects")
                                .font(.headline)
                            Image(uiImage: imageForAllSelectedObjects)
                                .resizable()
                                .scaledToFit()
                                .padding()
                                .background {
                                    RoundedRectangle(cornerRadius: 10)
                                        .foregroundStyle(.teal)
                                }
                                .frame(height: 300)
                        }
                    }
                    
                    
                }
                .padding()
                
                Text("Detected objects count")
                    .font(.headline)
                
                Text("\(self.detectedObjects.count)")
                
                LazyVGrid(columns: [
                    .init(.flexible()),
                    .init(.flexible())
                ]) {
                    ForEach(self.detectedObjects.sorted(by: { one, two in
                        return one.bounds.minX < two.bounds.minX
                    }), id: \.hashValue) { object in
                        VStack(alignment: .leading) {
                            Text("Position: x \(object.bounds.origin.x) y \(object.bounds.origin.y)")
                            Text("Size: width \(object.bounds.width) height \(object.bounds.height)")
                            Text("Object hash: \(object.hashValue)")
                            // highlight
                            Button("Select") {
                                self.viewModel.interaction.highlightedSubjects.insert(object)
                                // generate an image with all currently highlighted objects
                                Task { @MainActor in
                                    do {
                                        try await generateImageForAllSelectedObjects()
                                    } catch {
                                        self.errorMessage = error.localizedDescription
                                    }
                                }
                            }
                            // extract this to an image
                            Button("Extract") {
                                Task { @MainActor in
                                    if let objectImage = try? await object.image {
                                        self.extractedObjectImage = objectImage
                                    }
                                }
                            }
                            // remove selection
                            Button("Un-select") {
                                self.viewModel.interaction.highlightedSubjects.remove(object)
                                // generate an image with all currently highlighted objects
                                Task { @MainActor in
                                    do {
                                        try await generateImageForAllSelectedObjects()
                                    } catch {
                                        self.errorMessage = error.localizedDescription
                                    }
                                }
                            }
                        }
                        .padding()
                        .background {
                            RoundedRectangle(cornerRadius: 20)
                                .foregroundStyle(Color(uiColor: .systemGroupedBackground))
                        }
                    }
                }
                
                Text("Long press on an object within the image to copy.")
                
            }
        }
        .alert(item: $errorMessage) { message in
            Alert(title: Text("Error while analyzing objects within the image"), message: Text(message))
        }
        
    }
    
    func generateImageForAllSelectedObjects() async throws {
        let allSubjectsImage = try await self.viewModel.interaction.image(for: self.viewModel.interaction.highlightedSubjects)
        self.imageForAllSelectedObjects = allSubjectsImage
    }
    
}

extension String: Identifiable {
    public var id: String { return self }
}
```

SwiftUIビューでは、アナライザークラスを直接呼び出すことができます。例えば、オブジェクトをハイライトするには、`self.viewModel.interaction.highlightedSubjects.insert(object)` を使用します。

ここでは、`.environmentObject` ビュー修飾子を使用して、`ObservableObject` を互換性ビューにリンクしています: `.environmentObject(viewModel)`

## タップされた位置のオブジェクトを見つける

ユーザーがどのオブジェクトをタップしたかを検出する機能も追加できます。
まず、タップジェスチャーレコグナイザーを画像ビューにアタッチします。

```swift
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
imageView.addGestureRecognizer(tapGesture)
```

`handleTap` 関数では、タップされた位置にサブジェクトがあるかどうかを確認します。次に、画像を抽出するか、(タップされた)サブジェクトのハイライトを付ける（または削除する）ことができます：

```swift
@objc func handleTap(_ sender: UITapGestureRecognizer) {
    guard let loadedImageView else { return }
    let point = sender.location(in: loadedImageView)
    Task { @MainActor in
        if let tappedSubject = await interaction.subject(at: point) {
            // select or de-select it
            if interaction.highlightedSubjects.contains(tappedSubject) {
                interaction.highlightedSubjects.remove(tappedSubject)
            } else {
                interaction.highlightedSubjects.insert(tappedSubject)
            }
        }
    }
}
```

SwiftUIでは、.onTapGestureビュー修飾子を直接使用してタップされた位置を読み取ることができます：

```swift
ObjectPickableImageView(imageObject: userPickedImage)
    .scaledToFit()
    .cornerRadius(20)
    .frame(height: 350)
    .environmentObject(viewModel)
    .onTapGesture { tappedLocation in
        Task { @MainActor in
            if let tappedSubject = await self.viewModel.interaction.subject(at: tappedLocation) {
                // select or de-select it
                if self.viewModel.interaction.highlightedSubjects.contains(tappedSubject) {
                    self.viewModel.interaction.highlightedSubjects.remove(tappedSubject)
                } else {
                    self.viewModel.interaction.highlightedSubjects.insert(tappedSubject)
                }
            }
        }
    }
```

これで、画像内のサブジェクトをタップしてハイライトを付けたり、削除したりできるようになります：

![visionkit-image-subject-extract-tap-highlight.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/667d41ac-cc06-4ade-8cc6-9daf04649070.gif)

# 方法2: Visionリクエストを使用する

上記の関数が呼び出す基盤となるAPIである `VNGenerateForegroundInstanceMaskRequest` を直接使用できます。
以下のように分析を実行できます：

```swift
func performAnalysis(forPicked: UIImage) throws {
    guard let ciImg = CIImage(image: forPicked) else {
        throw RequestError.failedToGetCIImage
    }
    let request = VNGenerateForegroundInstanceMaskRequest()
    let handler = VNImageRequestHandler(ciImage: ciImg)
    self.imageRequestHandler = handler
    
    try handler.perform([request])
    
    guard let result = request.results else {
        throw RequestError.noSubjectsDetected
    }
    
    DispatchQueue.main.async {
        self.imageAnalysisResults = result
    }
}

enum RequestError: Error, LocalizedError {
    case failedToGetCIImage
    case noSubjectsDetected
    
    var errorDescription: String? {
        switch self {
            case .failedToGetCIImage:
                return "Failed to get CIImage from the provided image."
            case .noSubjectsDetected:
                return "No subjects were detected in the image."
        }
    }
}
```

この関数は、ユーザーが選択した（またはアプリケーションが入力した）UIImageを受け取り、それをCIImageに変換し、Visionの前景オブジェクト認識リクエストを実行します。

## マスク画像の抽出

オブジェクトのマスクを取得できます。以下に示すように、マスクは前景オブジェクトが存在するピクセルを示します。ここで、白い部分は前景オブジェクトとして検出されたピクセルを示し、黒い部分は背景のピクセルを示します。

![cats-mask.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/4aad4da3-7fa1-157e-6e8a-41ff60211f1d.jpeg)

以下のコードでは、`convertMonochromeToColoredImage` 関数がプレビューマスク画像を生成するのに役立ちます。`apply` 関数は、マスクを元の入力画像に適用するのに役立ちます（これにより、背景なしの猫の画像のみを取得できます）。

```swift
guard let firstObservation = self.imageAnalysisResults.first,
      let imageRequestHandler,
      let mask = try? firstObservation.generateScaledMaskForImage(forInstances: firstObservation.allInstances, from: imageRequestHandler) else {
    return
}
let ciImg = CIImage(cvPixelBuffer: mask)
self.maskedImagePreview = convertMonochromeToColoredImage(monochromeImage: ciImg, color: .blue)
// Get the subject only image
guard let userPickedImage,
      let userPickedImage_CI = CIImage(image: userPickedImage) else {
    return
}
guard let subjectOnlyImage = apply(mask: ciImg, toImage: userPickedImage_CI, backgroundImage: self.userPickedBackgroundImage) else {
    return
}
self.extractedSubjectImage = subjectOnlyImage

func convertMonochromeToColoredImage(monochromeImage: CIImage, color: UIColor) -> UIImage? {
    // Create a color filter
    let colorFilter = CIFilter(name: "CIColorMonochrome")
    colorFilter?.setValue(monochromeImage, forKey: kCIInputImageKey)
    colorFilter?.setValue(CIColor(color: color), forKey: kCIInputColorKey)
    colorFilter?.setValue(1.0, forKey: kCIInputIntensityKey)
    
    // Get the output CIImage from the filter
    guard let outputImage = colorFilter?.outputImage else {
        return nil
    }
    
    // Convert the CIImage to UIImage
    let context = CIContext()
    if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
        return UIImage(cgImage: cgImage)
    }
    
    return nil
}

func apply(mask: CIImage, toImage image: CIImage, backgroundImage: UIImage? = nil) -> UIImage? {
    // Convert the optional UIImage to CIImage and resize/crop it if provided
    let inputExtent = image.extent
    var backgroundTransformed: CIImage?
    if let backgroundImage = backgroundImage, let backgroundCIImage = CIImage(image: backgroundImage) {
        backgroundTransformed = backgroundCIImage
            .transformed(by: CGAffineTransform(scaleX: inputExtent.width / backgroundCIImage.extent.width, y: inputExtent.height / backgroundCIImage.extent.height))
            .cropped(to: inputExtent)
    }
    
    let filter = CIFilter(name: "CIBlendWithMask")
    filter?.setValue(image, forKey: kCIInputImageKey)
    filter?.setValue(mask, forKey: kCIInputMaskImageKey)
    if let backgroundTransformed = backgroundTransformed {
        filter?.setValue(backgroundTransformed, forKey: kCIInputBackgroundImageKey)
    }
    
    guard let outputCIImg = filter?.outputImage else {
        print("Error: Filter output image is nil")
        return nil
    }
    
    if outputCIImg.extent.isInfinite || outputCIImg.extent.isEmpty {
        print("Error: The resulting image has an invalid extent")
        return nil
    }
    
    // Convert the CIImage to UIImage
    let context = CIContext()
    guard let cgImage = context.createCGImage(outputCIImg, from: outputCIImg.extent) else {
        return nil
    }
    
    return UIImage(cgImage: cgImage)
}
```

`apply` 関数では、背景画像を供給することもできます。まず、その背景画像を元の画像に合わせてスケールおよびトリミングし、それを前景画像の背景として適用します。

![IMG_A752E3F394EA-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/46c14798-9b09-8749-5eb3-76947ae5cad4.jpeg)

![IMG_2097E2EEAB6F-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/5b78db4d-fa15-15af-389a-31a943496b13.jpeg)

## そうです！これが猫たちのパーティーを作る方法です！

完全なプロジェクトコード（SwiftUIで）はここにあります：
https://github.com/mszpro/LiftObjectFromImage

---
:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)
:relaxed: 個人ウェブサイト https://MszPro.com

![writing-quickly_emoji_400.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/640938e9-c121-8152-b8c4-8ab63124afcc.png)

Written by MszPro\~