Мобильные приложения в настоящее время поставляются с искусственным интеллектом, который позволяет пользователю достигать большего с меньшими трудностями. Некоторые из этих интеллектуальных функций представлены в виде распознавания объектов, анализа речи, сегментации изображений, предсказаний и многого другого. Есть два способа развертывания такой аналитики: в облаке или на устройстве. Последний предлагает лучший опыт, такой как работа в автономном режиме, сохранение конфиденциальности пользователя, поскольку данные не отправляются на сервер, более дешевая стоимость, поскольку разработчик тратит меньше на сервер, и более высокая производительность, поскольку нет сетевых вызовов.
Xamarin Forms — это кроссплатформенная платформа, принадлежащая Microsoft, которая позволяет разработчикам создавать нативные приложения, которые можно развернуть в различных системах: Android, iOS, Mac и Windows. ONNX — это формат, созданный Microsoft для экспорта моделей машинного обучения, созданных из различных сред, таких как PyTorch, Keras, SciKit-learn и т. д., и предоставляет среду выполнения для запуска указанных моделей в различных средах.
Давайте посмотрим, как мы можем интегрировать среду выполнения ONNX в приложение Xamarin, чтобы обеспечить распознавание объектов. Мы будем использовать специально обученную модель для обнаружения людей на фотографии, но не стесняйтесь загружать любую модель из зоопарка моделей ONNX, например модель MobileNet.
Настройка проекта
Во-первых, убедитесь, что вы установили Visual Studio (VS) 2017/2019 для Windows/Mac с мобильной разработкой с установленной рабочей нагрузкой .NET. Обратитесь к документации за руководствами по установке, затем создайте пустой проект Xamarin Forms с помощью VS, как показано ниже.
Установите некоторые необходимые зависимости с помощью диспетчера пакетов NuGet: ONNX Runtime версии 1.10.0 или выше и Skiasharp версии 2.80.3 или выше. Skiasharp — это библиотека 2D-графики, которая позже поможет на этапе предварительной/постобработки.
Для проекта iOS требуется, чтобы он был нацелен на минимальную версию 11.0. Для этого откройте файл info.plist в проекте iOS и соответствующим образом измените минимальный номер версии.
Давайте добавим ONNX в общий проект в качестве встроенного ресурса. Встроенные ресурсы предлагают интуитивно понятный способ добавления и доступа к локальным файлам в ваше приложение. Дополнительные сведения см. в документации. Создайте папку Resources в общем проекте и добавьте туда свою модель. Убедитесь, что вы установили действие сборки для этого файла как встроенное, как показано ниже.
Наконец, нам нужно настроить проект для использования C# версии 8 или выше, чтобы использовать новые функции языка, такие как использование объявлений. Версия, которую вы выберете, зависит от установленной версии .NET, и вы можете обратиться к этой документации для получения дополнительной информации.
Интеграция машинного обучения
Давайте создадим класс C# с выделенным кодом в общем проекте под названием inference. Целью этого класса будет выполнение четырех основных функций: инициализация модели в сеансе вывода, предварительная обработка изображения, выполнение вывода по изображению и постобработка результата.
Процесс инициализации будет реализован асинхронно вне основного потока, который обычно запускает пользовательский интерфейс, что обеспечивает бесперебойную работу интерфейса.
Изображения должны быть предварительно обработаны таким образом, чтобы они соответствовали входным требованиям модели, и в этом сценарии есть две основные проблемы. Во-первых, необходимо изменить размер изображения, чтобы оно соответствовало размерам изображений, используемых при обучении модели. Чтобы узнать размеры, загрузите свою модель машинного обучения в Neutron, приложение, которое визуализирует архитектуру модели и отмечает размер используемых входных данных. Для модели, которую мы выбрали, входные размеры составляют 256 x 512. Во-вторых, данные изображения содержат значения RGB, хранящиеся в многомерном массиве, и модель принимает тензоры определенной формы. Используя приложение Neutron, вы можете определить форму тензора, и для нашего случая это 1 x 3 x 512 x 256.
Как только изображение представлено в виде тензора, вы можете выполнить вывод на изображении, используя сеанс вывода, созданный в функции инициализации. На выходе получается тензор, имеющий форму, показанную в сетевой архитектуре. Вы можете выбрать выполнение логического вывода для ЦП или ГП, настроив свойство устройства среды выполнения ONNX.
Процесс машинного обучения цикличен — найдите подходящий рабочий процесс. Сотрудничайте между командами, воспроизводите эксперименты и выполняйте другие действия с помощью надежной стратегии MLOps. Ознакомьтесь с нашими экспертными решениями для решения распространенных проблем команды машинного обучения.
Наконец, вам нужно будет обработать результаты вывода и сопоставить их с меткой, которую вы загрузили как внешний ресурс. Кроме того, поскольку это задача обнаружения объекта, нам нужно отобразить ограничивающую рамку вокруг обнаруженного объекта. Это потребует использования координат ограничительной рамки (x1, x2, y1, y2) и их повторного масштабирования, чтобы они соответствовали исходному размеру изображения.
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Threading.Tasks; | |
using Microsoft.ML.OnnxRuntime; | |
using Microsoft.ML.OnnxRuntime.Tensors; | |
using SkiaSharp; | |
namespace onnx_object_detection | |
{ | |
class PersonDetector | |
{ | |
const int BatchSize = 1; | |
const int NumberOfChannels = 3; | |
const int ImageSizeX = 256; | |
const int ImageSizeY = 512; | |
const string ModelInputName = "input"; | |
const string ModelOutputName = "output"; | |
byte[] _model; | |
List<string> _labels; | |
InferenceSession _session; | |
Task _initTask; | |
public PersonDetector() | |
{ | |
_ = InitAsync(); | |
} | |
public async Task<string> GetClassificationAsync(byte[] image) | |
{ | |
await InitAsync().ConfigureAwait(false); | |
using var sourceBitmap = SKBitmap.Decode(image); | |
var pixels = sourceBitmap.Bytes; | |
// Preprocess image data according to model requirements | |
// Scale and crop the original image if necessary to match the way the model has been trained | |
// In this case, the model expects 256x512 images | |
if (sourceBitmap.Width != ImageSizeX || sourceBitmap.Height != ImageSizeY) | |
{ | |
float ratio = (float)Math.Min(ImageSizeX, ImageSizeY) / Math.Min(sourceBitmap.Width, sourceBitmap.Height); | |
using SKBitmap scaledBitmap = sourceBitmap.Resize(new SKImageInfo( | |
(int)(ratio * sourceBitmap.Width), | |
(int)(ratio * sourceBitmap.Height)), | |
SKFilterQuality.Medium); | |
var horizontalCrop = scaledBitmap.Width - ImageSizeX; | |
var verticalCrop = scaledBitmap.Height - ImageSizeY; | |
var leftOffset = horizontalCrop == 0 ? 0 : horizontalCrop / 2; | |
var topOffset = verticalCrop == 0 ? 0 : verticalCrop / 2; | |
var cropRect = SKRectI.Create( | |
new SKPointI(leftOffset, topOffset), | |
new SKSizeI(ImageSizeX, ImageSizeY)); | |
using SKImage currentImage = SKImage.FromBitmap(scaledBitmap); | |
using SKImage croppedImage = currentImage.Subset(cropRect); | |
using SKBitmap croppedBitmap = SKBitmap.FromImage(croppedImage); | |
pixels = croppedBitmap.Bytes; | |
} | |
// Preprocess the image data into the format expected by the model. | |
// The loop below iterates over the image pixels one row at a time, | |
// applies the requisite normalization to each RGB value, then stores each in the channelData array. | |
// The channelData array stores the normalized RGB values sequentially one channel at a time (instead of the original RGB, RGB, ... sequence) i.e. | |
// first all the R values, | |
// then all the G values, | |
// then all the B values | |
// The resulting channelData array is used to create the requisite Tensor object as input to the InferenceSession.Run method | |
var bytesPerPixel = sourceBitmap.BytesPerPixel; | |
var rowLength = ImageSizeX * bytesPerPixel; | |
var channelLength = ImageSizeX * ImageSizeY; | |
var channelData = new float[channelLength * 3]; | |
var channelDataIndex = 0; | |
for (int y = 0; y < ImageSizeY; y++) | |
{ | |
var rowOffset = y * rowLength; | |
for (int x = 0, columnOffset = 0; x < ImageSizeX; x++, columnOffset += bytesPerPixel) | |
{ | |
var pixelOffset = rowOffset + columnOffset; | |
var pixelR = pixels[pixelOffset]; | |
var pixelG = pixels[pixelOffset + 1]; | |
var pixelB = pixels[pixelOffset + 2]; | |
var rChannelIndex = channelDataIndex; | |
var gChannelIndex = channelDataIndex + channelLength; | |
var bChannelIndex = channelDataIndex + (channelLength * 2); | |
channelData[rChannelIndex] = pixelR; | |
channelData[gChannelIndex] = pixelG; | |
channelData[bChannelIndex] = pixelB; | |
channelDataIndex++; | |
} | |
} | |
// Create Tensor model input | |
// The model expects input to be in the shape of (N x 3 x H x W) i.e. | |
// mini-batches (where N is the batch size) of 3-channel RGB images with H and W of 256 and 512 | |
// https://onnxruntime.ai/docs/api/csharp-api#systemnumericstensor | |
var input = new DenseTensor<float>(channelData, new[] { BatchSize, NumberOfChannels, ImageSizeX, ImageSizeY }); | |
// Run inferencing | |
// https://onnxruntime.ai/docs/api/csharp-api#methods-1 | |
// https://onnxruntime.ai/docs/api/csharp-api#namedonnxvalue | |
using var results = _session.Run(new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor(ModelInputName, input) }); | |
// Resolve model output | |
// https://onnxruntime.ai/docs/api/csharp-api#disposablenamedonnxvalue | |
var output = results.FirstOrDefault(i => i.Name == ModelOutputName); | |
if (output == null) | |
return "Unknown"; | |
// Postprocess output (get highest score and corresponding label) | |
// https://github.com/onnx/models/tree/master/vision/classification/mobilenet#postprocessing | |
var boundingBox = output.AsTensor<float>().ToList(); | |
var cordinates = float[4]; | |
cordinates[0] = boundingBox.ElementAt(0); | |
cordinates[1] = boundingBox.ElementAt(1); | |
cordinates[2] = boundingBox.ElementAt(2); | |
cordinates[3] = boundingBox.ElementAt(3); | |
return cordinates; | |
} | |
Task InitAsync() | |
{ | |
if (_initTask == null || _initTask.IsFaulted) | |
_initTask = InitTask(); | |
return _initTask; | |
} | |
async Task InitTask() | |
{ | |
var assembly = GetType().Assembly; | |
// Get model | |
using var modelStream = assembly.GetManifestResourceStream($"{assembly.GetName().Name}.klurdy_person_detection.onnx"); | |
using var modelMemoryStream = new MemoryStream(); | |
modelStream.CopyTo(modelMemoryStream); | |
_model = modelMemoryStream.ToArray(); | |
// Create InferenceSession (runtime representation of the model with optional SessionOptions) | |
// This can be reused for multiple inferences to avoid unnecessary allocation/dispose overhead | |
// https://onnxruntime.ai/docs/api/csharp-api#inferencesession | |
// https://onnxruntime.ai/docs/api/csharp-api#sessionoptions | |
_session = new InferenceSession(_model); | |
} | |
} | |
} |
Разработка пользовательского интерфейса
Давайте создадим простой пользовательский интерфейс с кнопкой для загрузки изображения и заполнителем изображения для отображения загруженного изображения. Xamarin Forms использует XAML в качестве языка разметки для создания интерфейсов, которые впоследствии будут скомпилированы в собственные элементы для iOS и Android.
<?xml version="1.0" encoding="utf-8" ?> | |
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" | |
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | |
x:Class="onnx_object_detection.MainPage"> | |
<Grid> | |
<Grid.RowDefinitions > | |
<RowDefinition Height="*" /> | |
<RowDefinition Height="Auto" /> | |
</Grid.RowDefinitions> | |
<Image x:Name="PhotoImage" /> | |
<Button x:Name="CameraButton" Text="Take Photo" Grid.Row="1" /> | |
</Grid> | |
</ContentPage> |
Затем добавьте некоторую логику для привязки элементов, добавленных к интерфейсу, к классу вывода, чтобы добавить интерактивности.
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
using Xamarin.Essentials; | |
using Xamarin.Forms; | |
namespace onnx_object_detection | |
{ | |
public partial class MainPage : ContentPage | |
{ | |
PersonDetector _detector; | |
String PhotoPath; | |
public MainPage() | |
{ | |
InitializeComponent(); | |
_detector = new PersonDetector(); | |
CameraButton.Clicked += CameraButton_Clicked; | |
} | |
private async void CameraButton_Clicked(object sender, EventArgs e) | |
{ | |
await TakePhotoAsync(); | |
} | |
async Task RunInferenceAsync(String imagePath) | |
{ | |
CameraButton.IsEnabled = false; | |
try | |
{ | |
// var sampleImage = await _detector.GetSampleImageAsync(); | |
var result = await _detector.GetClassificationAsync(imagePath); | |
await DisplayAlert("Result", result, "OK"); | |
} | |
catch (Exception ex) | |
{ | |
await DisplayAlert("Error", ex.Message, "OK"); | |
} | |
finally | |
{ | |
CameraButton.IsEnabled = true; | |
} | |
} | |
async Task TakePhotoAsync() | |
{ | |
try | |
{ | |
var photo = await MediaPicker.CapturePhotoAsync(); | |
await LoadPhotoAsync(photo); | |
Console.WriteLine($"CapturePhotoAsync COMPLETED: {PhotoPath}"); | |
} | |
catch (FeatureNotSupportedException fnsEx) | |
{ | |
// Feature is not supported on the device | |
} | |
catch (PermissionException pEx) | |
{ | |
// Permissions not granted | |
} | |
catch (Exception ex) | |
{ | |
Console.WriteLine($"CapturePhotoAsync THREW: {ex.Message}"); | |
} | |
} | |
async Task LoadPhotoAsync(FileResult photo) | |
{ | |
// canceled | |
if (photo == null) | |
{ | |
PhotoPath = null; | |
return; | |
} | |
// Load Photo Into UI Placeholder | |
PhotoImage.Source = ImageSource.FromStream(() => { return await photo.OpenReadAsync(); }); | |
// save the file into local storage | |
await RunInferenceAsync(photo.FullPath); | |
} | |
} | |
} |
Заключение
Добавить искусственный интеллект в ваши приложения Xamarin с помощью среды выполнения ONNX довольно просто, и производительность вполне приемлемая. Вы можете оптимизировать свои модели с помощью методов квантования на этапе обучения, что имеет тенденцию уменьшать размер модели, чтобы гарантировать, что упакованное приложение меньше и работает лучше.
Примечание редактора. Heartbeat — это интернет-издание и сообщество, созданное участниками и посвященное предоставлению лучших образовательных ресурсов для специалистов по науке о данных, машинному обучению и глубокому обучению. Мы стремимся поддерживать и вдохновлять разработчиков и инженеров из всех слоев общества.
Независимая от редакции, Heartbeat спонсируется и публикуется Comet, платформой MLOps, которая позволяет специалистам по данным и командам машинного обучения отслеживать, сравнивать, объяснять и оптимизировать свои эксперименты. Мы платим нашим авторам и не продаем рекламу.
Если вы хотите внести свой вклад, перейдите к нашему призыву к участию. Вы также можете подписаться на получение наших еженедельных информационных бюллетеней (Еженедельник глубокого обучения и Информационный бюллетень Comet), присоединиться к нам в Slack и следить за Comet в Twitter и LinkedIn, чтобы получать ресурсы, события и многое другое, что поможет вам быстрее создавать лучшие модели машинного обучения.