[Эту статью также можно прочитать здесь]

Допустим, мы хотим создать приложение или функцию, которая импортирует фотографию, а затем классифицирует изображение. Например, если мы импортируем изображение собаки, приложение должно распознать, что это собака.

Как мы можем сделать это в iOS?

Во-первых, давайте разберем функцию. Нам понадобятся следующие два компонента:

  1. Сервис классификации изображений
  2. Средство выбора изображений, которое импортирует фото (предположительно из Фото).

Начнем с первого — создания службы классификации.

Для выполнения алгоритмов машинного обучения нам необходимо предоставить обученную модель. ML Model — это, по сути, часть программного обеспечения, которое специально обучено чему-то одному. Обычно распознает закономерность. Чтобы наше приложение могло определить, действительно ли изображение является определенным объектом, нам нужен Core ML Model, обученный классифицировать изображения.

Есть несколько моделей, предоставленных Apple, которые мы можем использовать здесь. В этом примере проекта мы используем MobileNetV2, но не стесняйтесь использовать другие.

Если вам нужны другие модели Core ML, не стесняйтесь искать на GitHub или на этой странице.

Затем давайте загрузим эту модель Core ML и импортируем ее в наш проект.

При нажатии на модель ML в Xcode также отображается соответствующая информация для обученной модели.

Чтобы использовать это машинное обучение, Apple предоставляет API под названием Vision. Vision содержит алгоритмы, которые выполняют задачи с изображениями и видео. Для этого нам нужно определить запрос, основанный на VNCoreMLRequest

Давайте сначала определим, что нужно нашему приложению от службы классификации.

protocol ClassificationServiceProviding {
    var classificationsResultPub: Published<String>.Publisher { get }
    func updateClassifications(for image: UIImage)
}

Мы знаем, что служба должна предоставить нам возможность начать процесс классификации изображения и возможность получить результаты.

Теперь давайте реализуем этот сервис, а также файл VNCoreMLRequest.

final class ClassificationService: ClassificationServiceProviding {
    
    @Published private var classifications: String = ""
    var classificationsResultPub: Published<String>.Publisher { $classifications }
    
    /// - Tag: MLModelSetup
    lazy var classificationRequest: `VNCoreMLRequest` = {
        do {
            let model = try VNCoreMLModel(for: MobileNetV2(configuration: MLModelConfiguration()).model)
            
            let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in
                self?.processClassifications(for: request, error: error)
            })
            request.imageCropAndScaleOption = .centerCrop
            return request
        } catch {
            fatalError("Failed to load Vision ML model: \(error)")
        }
    }()
    
    
    // MARK: - Image Classification
    
    /// - Tag: PerformRequests
    func updateClassifications(for image: UIImage) {
        let orientation = CGImagePropertyOrientation(image.imageOrientation)
        guard let ciImage = CIImage(image: image) else { fatalError("Unable to create \(CIImage.self) from \(image).") }
        
        /// Clear old classifications
        self.classifications = ""
        
        DispatchQueue.global(qos: .userInitiated).async {
            let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
            
            do {
                try handler.perform([self.classificationRequest])
            } catch {
                print("Failed to perform classification.\n\(error.localizedDescription)")
            }
        }
    }
    
    /// Updates the variable with the results of the classification.
    /// - Tag: ProcessClassifications
    private func processClassifications(for request: VNRequest, error: Error?) {
        DispatchQueue.main.async {
            guard let results = request.results else {
                return
            }
            // The `results` will always be `VNClassificationObservation`s, as specified by the Core ML model in this project.
            let classifications = results as! [VNClassificationObservation]
            
            if classifications.isEmpty {
                // do nothing
            } else {
                // Display top classifications ranked by confidence in the UI.
                let topClassifications = classifications.prefix(5)
                let descriptions = topClassifications.map { classification in
                    // Formats the classification for display; e.g. "(0.37) cliff, drop, drop-off".
                    return String(format: "(%.2f) %@\n", classification.confidence, classification.identifier)
                }
                
                self.classifications = descriptions.joined(separator: " ")
            }
        }
    }
}

Давайте разберем это.

Во-первых, мы определили переменную с именем classificationRequest типа VNCoreMLRequest. Здесь мы указываем, что хотим использовать импортированную модель ML, и каждый раз при обработке запроса отправляем результаты в функцию processClassifications.

Затем у нас есть функция updateClassifications, которая позволяет клиентам/ViewModels/магазинам обновлять классификации определенного изображения. Таким образом, если изображение задано, оно выполнит запрос Vision Image на основе типа VNImageRequestHandler. Это принимает два параметра типа CIImage и CGImagePropertyOrientation.

Чтобы помочь нам определить ориентацию изображения, Apple предоставила расширение для достижения этой цели.

///
/// https://developer.apple.com/documentation/imageio/cgimagepropertyorientation
///
extension CGImagePropertyOrientation {
    /**
     Converts a `UIImageOrientation` to a corresponding
     `CGImagePropertyOrientation`. The cases for each
     orientation are represented by different raw values.
     
     - Tag: ConvertOrientation
     */
    init(_ orientation: UIImage.Orientation) {
        switch orientation {
        case .up: self = .up
        case .upMirrored: self = .upMirrored
        case .down: self = .down
        case .downMirrored: self = .downMirrored
        case .left: self = .left
        case .leftMirrored: self = .leftMirrored
        case .right: self = .right
        case .rightMirrored: self = .rightMirrored
        @unknown default:
            fatalError()
        }
    }
}

Затем обработчик запроса изображения выполняет алгоритм, который является нашей моделью ML, которую мы определили как classificationRequest.

Наконец, после завершения нашего classificationRequest вызывается processClassifications, и соответственно публикуются результаты для отображения.

Теперь, когда у нас есть служба, давайте определим наш ViewModel для управления подключением пользовательского интерфейса и службы.

import Combine
import UIKit

@MainActor
final class ContentViewModel: ObservableObject {
    @Published var displayImagePicker: Bool = false
    
    @Published var importedImage: UIImage? = nil
    
    @Published var classifications: String = ""
    
    let service: ClassificationServiceProviding
    
    private var subscribers: [AnyCancellable] = []
    
    init(
        image: UIImage? = nil,
        service: ClassificationServiceProviding = ClassificationService()
    ) {
        self.importedImage = image
        self.service = service
        
        self.subscribe()
        self.onChangeImage()
    }
    
    func subscribe() {
        self.service.classificationsResultPub
            .receive(on: DispatchQueue.main)
            .sink { [weak self] newClassifications in
                self?.classifications = newClassifications
            }
            .store(in: &subscribers)
    }
    
    func onChangeImage() {
        guard let image = importedImage else { return }
        service.updateClassifications(for: image)
    }
}

Здесь не нужно много объяснять, мы определили некоторые опубликованные переменные для отображения определенных элементов пользовательского интерфейса, а наша ViewModel зависит от класса, совместимого с протоколом ClassificationServiceProviding.

Как только пользовательский интерфейс получает/изменяет импортированное изображение, мы вызываем onChangeImage(), чтобы начать процесс классификации.

Теперь давайте перейдем к нашему представлению SwiftUI.

SwiftUI не имеет встроенного ImagePicker, поэтому мы должны использовать компонент UIKit под названием PHPickerViewController.

Без изучения того, как это сделать, уже есть отличная статья Hacking with Swift, посвященная тому, как этого добиться.

Давайте воспользуемся ImagePicker из этой статьи и создадим следующее представление SwiftUI.

struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()
    
    var body: some View {
        NavigationView {
            if let image = viewModel.importedImage {
                VStack(alignment: .leading) {
                    Image(uiImage: image)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous))
                        .padding()
                        .onTapGesture {
                            viewModel.displayImagePicker.toggle()
                        }
                        
                    ScrollView {
                        Text(viewModel.classifications)
                            .bold()
                            .padding()
                    }
                }
            } else {
                VStack {
                    Image(systemName: "photo.fill")
                        .imageScale(.large)
                        .foregroundColor(.accentColor)
                    
                    Button {
                        viewModel.displayImagePicker.toggle()
                    } label: {
                        Text("Pick an image")
                            .bold()
                            .frame(maxWidth: .infinity)
                            .padding()
                            .background(Color.accentColor)
                            .foregroundColor(.white)
                            .cornerRadius(16)
                    }
                }
                .padding()
            }
        }
        .onChange(of: viewModel.importedImage) { _ in viewModel.onChangeImage() }
        .sheet(isPresented: $viewModel.displayImagePicker) {
            ImagePicker(image: $viewModel.importedImage)
        }
    }
}

Давайте запустим приложение, импортируем изображение и посмотрим, что произойдет.

Мы видим импортированное изображение в приложении, а также результаты классификации.

Используемое изображение предоставлено Unsplash by Matt Drenth

Поздравляем! Теперь вы узнали, как использовать модель машинного обучения для классификации импортированного изображения в приложении iOS/iPadOS.

Надеюсь, теперь у вас есть представление о том, с чего начать работу с Core ML, Vision и классификациями изображений. Продолжайте создавать отличные вещи!

Полный пример проекта с исходным кодом доступен здесь, на GitHub.

Подпишись на меня в Твиттере"