Photosアプリでは、画像からオブジェクト(動物、人など)を抽出するために長押しすることができます。本記事では、この機能をImageAnalysisInteractionを使用して実装する方法について説明します。また、VNGenerateForegroundInstanceMaskRequest
を呼び出す方法についても詳しく掘り下げます。
この記事はUIKitとSwiftUIの両方のアプリケーションに適用されます。
- 与えられた画像内のオブジェクトを検出する
- コード内で異なるオブジェクトをハイライトする
- オブジェクトの画像を取得する
この記事のボーナスとして、以下の方法も紹介します:
- タップした位置のオブジェクトを取得する
- 被写体の背後の画像背景を置き換える
注意:この記事内のコードはシミュレータでは動作しません。
物理デバイスを使用してテストしてください。
SwiftUIのコードはUIKitのコードの後に続きます。
さあ、始めましょう!
方法 1: UIImageViewに画像解析コンポーネントをアタッチ
画像内のオブジェクトを検出
画像解析を実行するには、UIImageView
に ImageAnalysisInteraction
を追加する必要があります。
private let imageView = UIImageView()
private let interaction = ImageAnalysisInteraction()
imageView.image = imageObject
imageView.contentMode = .scaleAspectFit
interaction.preferredInteractionTypes = .imageSubject
imageView.addInteraction(interaction)
interaction
変数には関数全体でアクセスする必要があるため、
これらをビューまたはビュー・モデルの変数として保存する必要があります。
ここでは、好みのインタラクションタイプを設定できます。AppleのPhotosアプリを使用すると、画像内のオブジェクトだけでなく、テキストやQRコードも選択できることがわかります。これはpreferredInteractionTypesプロパティを使用して定義されます。このプロパティに配列を提供することで、ユーザーがアプリの画像ビューでどのオブジェクトと対話できるかを設定できます。
interaction.preferredInteractionTypes = [.dataDetectors, .imageSubject, .textSelection, .visualLookUp]
.dataDetectors
は、URL、メールアドレス、物理アドレスを意味します。
.imageSubject
は、画像内のオブジェクトを意味します(この記事の主な焦点)。
.textSelection
は、画像内のテキストを選択することを意味します。
.visualLookUp
は、iOSシステムが詳細情報を表示できるオブジェクトを意味します(例:猫や犬の品種)。
この記事では、これを .imageSubject
のみに設定できます。
画像解析の実行
画像解析を実行して、画像内のどのオブジェクトがあるかを確認するには、以下のコードを実行します:
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
}
}
analyzer
変数には関数全体でアクセスする必要があるため、
これらをビューまたはビュー・モデルの変数として保存する必要があります。
interaction.highlightedSubjects
プロパティを使用して、検出された各オブジェクトまたはすべてのオブジェクトをハイライトすることもできます。上記のコードでは、この変数を detectedSubjects
に設定すると、検出されたすべてのオブジェクトがハイライトされます。
画像解析からデータを読み取る
interaction.subjectsプロパティとinteraction.highlightedSubjectsプロパティを使用して、コード内でオブジェクトを読み取り、ハイライトするサブジェクトを設定できます。各サブジェクト(ImageAnalysisInteraction.Subjectに準拠)内で、サイズ、原点(バウンディングボックス)を読み取り、画像を抽出することができます。
サイズとバウンディングボックスへのアクセス:
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)")
}
オブジェクトの切り抜かれた画像を取得するには、
Task { @MainActor in
if let objectImage = try? await object.image {
self.extractedObjectImage = objectImage
}
}
オブジェクトを選択済み (highlight) としてマークするには:
interaction.highlightedSubjects.insert(object)
ハイライトリストからオブジェクトを削除するには:
interaction.highlightedSubjects.remove(object)
// interaction.highlightedSubjects = []
ハイライトされた(選択された)オブジェクトのための単一の画像を取得する
任意のオブジェクトを組み合わせて単一の画像を取得することもできます。
例えば、現在ハイライトされている左端の猫と右端の猫の画像を取得できます。
以下のコードでは、すべてのハイライトされたオブジェクト(interaction.highlightedSubjects)を使用して interaction.image
関数を呼び出します:
func generateImageForAllSelectedObjects() async throws {
let allSubjectsImage = try await interaction.image(for: interaction.highlightedSubjects)
self.imageForAllSelectedObjects = allSubjectsImage
}
SwiftUI互換ビュー
上記のロジックをSwiftUIで使用する場合、3つのファイルを書くことができます。
ここでは、SwiftUIビューImageAnalysisViewModelと互換ビューObjectPickableImageViewの間でデータを共有するのに役立つObservableObjectを示します。
ObservableObject:
@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
}
}
互換ビュー:
@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ビュー:
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)
タップされた位置のオブジェクトを見つける
ユーザーがどのオブジェクトをタップしたかを検出する機能も追加できます。
まず、タップジェスチャーレコグナイザーを画像ビューにアタッチします。
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
imageView.addGestureRecognizer(tapGesture)
handleTap
関数では、タップされた位置にサブジェクトがあるかどうかを確認します。次に、画像を抽出するか、(タップされた)サブジェクトのハイライトを付ける(または削除する)ことができます:
@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ビュー修飾子を直接使用してタップされた位置を読み取ることができます:
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)
}
}
}
}
これで、画像内のサブジェクトをタップしてハイライトを付けたり、削除したりできるようになります:
方法2: Visionリクエストを使用する
上記の関数が呼び出す基盤となるAPIである VNGenerateForegroundInstanceMaskRequest
を直接使用できます。
以下のように分析を実行できます:
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の前景オブジェクト認識リクエストを実行します。
マスク画像の抽出
オブジェクトのマスクを取得できます。以下に示すように、マスクは前景オブジェクトが存在するピクセルを示します。ここで、白い部分は前景オブジェクトとして検出されたピクセルを示し、黒い部分は背景のピクセルを示します。
以下のコードでは、convertMonochromeToColoredImage
関数がプレビューマスク画像を生成するのに役立ちます。apply
関数は、マスクを元の入力画像に適用するのに役立ちます(これにより、背景なしの猫の画像のみを取得できます)。
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
関数では、背景画像を供給することもできます。まず、その背景画像を元の画像に合わせてスケールおよびトリミングし、それを前景画像の背景として適用します。
そうです!これが猫たちのパーティーを作る方法です!
完全なプロジェクトコード(SwiftUIで)はここにあります:
https://github.com/mszpro/LiftObjectFromImage
Twitter @MszPro
個人ウェブサイト https://MszPro.com
Written by MszPro~