[Эту статью также можно прочитать здесь]
Допустим, мы хотим создать приложение или функцию, которая импортирует фотографию, а затем классифицирует изображение. Например, если мы импортируем изображение собаки, приложение должно распознать, что это собака.
Как мы можем сделать это в iOS?
Во-первых, давайте разберем функцию. Нам понадобятся следующие два компонента:
- Сервис классификации изображений
- Средство выбора изображений, которое импортирует фото (предположительно из Фото).
Начнем с первого — создания службы классификации.
Для выполнения алгоритмов машинного обучения нам необходимо предоставить обученную модель. 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.
Подпишись на меня в Твиттере"