diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample.sln b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample.sln
new file mode 100644
index 000000000..afe383e14
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample.sln
@@ -0,0 +1,27 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31611.283
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MauiVisionSample", "MauiVisionSample\MauiVisionSample.csproj", "{B181D00F-FFC0-489C-A59B-27665CD58015}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {B181D00F-FFC0-489C-A59B-27665CD58015}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B181D00F-FFC0-489C-A59B-27665CD58015}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B181D00F-FFC0-489C-A59B-27665CD58015}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {B181D00F-FFC0-489C-A59B-27665CD58015}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B181D00F-FFC0-489C-A59B-27665CD58015}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B181D00F-FFC0-489C-A59B-27665CD58015}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {61F7FB11-1E47-470C-91E2-47F8143E1572}
+ EndGlobalSection
+EndGlobal
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/App.xaml b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/App.xaml
new file mode 100644
index 000000000..a5e3f56e5
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/App.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/App.xaml.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/App.xaml.cs
new file mode 100644
index 000000000..d35e96edf
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/App.xaml.cs
@@ -0,0 +1,11 @@
+namespace MauiVisionSample;
+
+public partial class App : Application
+{
+ public App()
+ {
+ InitializeComponent();
+
+ MainPage = new AppShell();
+ }
+}
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/AppShell.xaml b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/AppShell.xaml
new file mode 100644
index 000000000..8aee3ef57
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/AppShell.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/AppShell.xaml.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/AppShell.xaml.cs
new file mode 100644
index 000000000..c2cd6a274
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/AppShell.xaml.cs
@@ -0,0 +1,9 @@
+namespace MauiVisionSample;
+
+public partial class AppShell : Shell
+{
+ public AppShell()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/ExecutionProviders.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/ExecutionProviders.cs
new file mode 100644
index 000000000..b0dc39395
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/ExecutionProviders.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace MauiVisionSample
+{
+ public enum ExecutionProviders
+ {
+ CPU, // CPU execution provider is always available by default
+ NNAPI, // NNAPI is available on Android
+ CoreML // CoreML is available on iOS/macOS
+ }
+}
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/IVisionSample.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/IVisionSample.cs
new file mode 100644
index 000000000..a9eda3e34
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/IVisionSample.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Threading.Tasks;
+
+namespace MauiVisionSample
+{
+ public interface IVisionSample
+ {
+ string Name { get; }
+ string ModelName { get; }
+ Task InitializeAsync();
+ Task UpdateExecutionProviderAsync(ExecutionProviders executionProvider);
+ Task ProcessImageAsync(byte[] image);
+ }
+}
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MainPage.xaml b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MainPage.xaml
new file mode 100644
index 000000000..226ce8012
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MainPage.xaml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MainPage.xaml.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MainPage.xaml.cs
new file mode 100644
index 000000000..4f302b166
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MainPage.xaml.cs
@@ -0,0 +1,319 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace MauiVisionSample;
+
+//using Microsoft.Maui.Platform;
+//using Microsoft.ML.OnnxRuntime;
+
+enum ImageAcquisitionMode
+{
+ Sample,
+ Capture,
+ Pick
+}
+
+public partial class MainPage : ContentPage
+{
+ IVisionSample _mobilenet;
+ IVisionSample _ultraface;
+
+ IVisionSample Mobilenet => _mobilenet ??= new MobilenetSample();
+ IVisionSample Ultraface => _ultraface ??= new UltrafaceSample();
+
+ public MainPage()
+ {
+ InitializeComponent();
+
+ // See:
+ // ONNX Runtime Execution Providers: https://onnxruntime.ai/docs/execution-providers/
+ // Core ML: https://developer.apple.com/documentation/coreml
+ // NNAPI: https://developer.android.com/ndk/guides/neuralnetworks
+ ExecutionProviderOptions.Items.Add(nameof(ExecutionProviders.CPU));
+
+ if (DeviceInfo.Platform == DevicePlatform.Android)
+ {
+ ExecutionProviderOptions.Items.Add(nameof(ExecutionProviders.NNAPI));
+ }
+
+ if (DeviceInfo.Platform == DevicePlatform.iOS)
+ {
+ ExecutionProviderOptions.Items.Add(nameof(ExecutionProviders.CoreML));
+ }
+
+ ExecutionProviderOptions.SelectedIndex = 0;
+
+ if (FileSystem.Current.AppPackageFileExistsAsync(MobilenetSample.ModelFilename).Result)
+ {
+ Models.Items.Add(Mobilenet.Name);
+ }
+
+ if (FileSystem.Current.AppPackageFileExistsAsync(UltrafaceSample.ModelFilename).Result)
+ {
+ Models.Items.Add(Ultraface.Name);
+ }
+
+ if (Models.Items.Any())
+ {
+ Models.SelectedIndex = Models.Items.IndexOf(Models.Items.First());
+ }
+ else
+ {
+ Models.IsEnabled = false;
+ }
+ }
+
+ protected override void OnAppearing()
+ {
+ base.OnAppearing();
+ ExecutionProviderOptions.SelectedIndexChanged += ExecutionProviderOptions_SelectedIndexChanged;
+ Models.SelectedIndexChanged += Models_SelectedIndexChanged;
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ ExecutionProviderOptions.SelectedIndexChanged -= ExecutionProviderOptions_SelectedIndexChanged;
+ Models.SelectedIndexChanged -= Models_SelectedIndexChanged;
+ }
+
+ async Task UpdateExecutionProviderAsync()
+ {
+ var executionProvider = ExecutionProviderOptions.SelectedItem switch
+ {
+ nameof(ExecutionProviders.CPU) => ExecutionProviders.CPU,
+ nameof(ExecutionProviders.NNAPI) => ExecutionProviders.NNAPI,
+ nameof(ExecutionProviders.CoreML) => ExecutionProviders.CoreML,
+ _ => ExecutionProviders.CPU
+ };
+
+ IVisionSample sample = Models.SelectedItem switch
+ {
+ MobilenetSample.Identifier => Mobilenet,
+ UltrafaceSample.Identifier => Ultraface,
+ _ => null
+ };
+
+ await sample.UpdateExecutionProviderAsync(executionProvider);
+ }
+
+ async Task AcquireAndAnalyzeImageAsync(ImageAcquisitionMode acquisitionMode = ImageAcquisitionMode.Sample)
+ {
+ byte[] outputImage = null;
+ string caption = null;
+
+ try
+ {
+ SetBusyState(true);
+
+ if (Models.Items.Count == 0 || Models.SelectedItem == null)
+ {
+ SetBusyState(false);
+ await DisplayAlert("No Samples", "Model files could not be found", "OK");
+ return;
+ }
+
+ var imageData = acquisitionMode switch
+ {
+ ImageAcquisitionMode.Capture => await TakePhotoAsync(),
+ ImageAcquisitionMode.Pick => await PickPhotoAsync(),
+ _ => await GetSampleImageAsync()
+ };
+
+ if (imageData == null)
+ {
+ SetBusyState(false);
+
+ if (acquisitionMode == ImageAcquisitionMode.Sample)
+ await DisplayAlert("No Sample Image", $"No sample image for {Models.SelectedItem}", "OK");
+
+ return;
+ }
+
+ ClearResult();
+
+ IVisionSample sample = Models.SelectedItem switch
+ {
+ MobilenetSample.Identifier => Mobilenet,
+ UltrafaceSample.Identifier => Ultraface,
+ _ => null
+ };
+
+ var result = await sample.ProcessImageAsync(imageData);
+
+ outputImage = result.Image;
+ caption = result.Caption;
+ }
+ finally
+ {
+ SetBusyState(false);
+ }
+
+ if (outputImage != null)
+ {
+ ShowResult(outputImage, caption);
+ }
+ }
+
+ Task GetSampleImageAsync() => Task.Run(() =>
+ {
+ var assembly = GetType().Assembly;
+
+ var imageName = Models.SelectedItem switch
+ {
+ MobilenetSample.Identifier => "wolves.jpg",
+ UltrafaceSample.Identifier => "satya.jpg",
+ _ => null
+ };
+
+ if (string.IsNullOrWhiteSpace(imageName))
+ {
+ return null;
+ }
+
+ return Utils.LoadResource(imageName).Result;
+ });
+
+ async Task PickPhotoAsync()
+ {
+ FileResult photo;
+
+ try
+ {
+ photo = await MediaPicker.PickPhotoAsync(new MediaPickerOptions { Title = "Choose photo" });
+ }
+ catch (FeatureNotSupportedException fnsEx)
+ {
+ throw new Exception("Feature is not supported on the device", fnsEx);
+ }
+ catch (PermissionException pEx)
+ {
+ throw new Exception("Permissions not granted", pEx);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"The {nameof(PickPhotoAsync)} method threw an exception", ex);
+ }
+
+ if (photo == null)
+ return null;
+
+ var bytes = await GetBytesFromPhotoFile(photo);
+
+ return Utils.HandleOrientation(bytes);
+ }
+
+ async Task TakePhotoAsync()
+ {
+ if (!MediaPicker.Default.IsCaptureSupported)
+ {
+ return null;
+ }
+
+ FileResult photo;
+
+ try
+ {
+ photo = await MediaPicker.Default.CapturePhotoAsync();
+ }
+ catch (FeatureNotSupportedException fnsEx)
+ {
+ throw new Exception("Feature is not supported on the device", fnsEx);
+ }
+ catch (PermissionException pEx)
+ {
+ throw new Exception("Permissions not granted", pEx);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"The {nameof(TakePhotoAsync)} method throw an exception", ex);
+ }
+
+ if (photo == null)
+ {
+#if WINDOWS
+ // MediaPicker CapturePhotoAsync does not work on Windows currently.
+ // https://github.com/dotnet/maui/issues/7616 eventually links to
+ // https://github.com/microsoft/WindowsAppSDK/issues/1034 which is apparently the cause.
+ // https://github.com/dotnet/maui/issues/7660#issuecomment-1152347557 has an example alternative
+ // implementation that could be used instead of MediaPicker.CapturePhotoAsync on Windows, although ideally
+ // MAUI would do that internally...
+ await DisplayAlert("Sorry", "Capturing a photo with MAUI MediaPicker does not work on Windows currently.", "OK");
+#elif IOS
+ if (Microsoft.Maui.Devices.DeviceInfo.Current.DeviceType == Microsoft.Maui.Devices.DeviceType.Virtual)
+ {
+ // https://github.com/dotnet/maui/issues/7013#issuecomment-1123384958
+ await DisplayAlert("Sorry", "Capturing a photo with MAUI MediaPicker is not possible with the iOS simulator.", "OK");
+ }
+#endif
+ // if it wasn't one of the above special cases the user most likely chose not to capture an image
+ return null;
+ }
+
+ var bytes = await GetBytesFromPhotoFile(photo);
+
+ return Utils.HandleOrientation(bytes);
+ }
+
+ async Task GetBytesFromPhotoFile(FileResult fileResult)
+ {
+ byte[] bytes;
+
+ using Stream stream = await fileResult.OpenReadAsync();
+ using MemoryStream ms = new MemoryStream();
+
+ stream.CopyTo(ms);
+ bytes = ms.ToArray();
+
+ return bytes;
+ }
+
+ void ClearResult() => MainThread.BeginInvokeOnMainThread(() =>
+ {
+ OutputImage.Source = null;
+ Caption.Text = string.Empty;
+ });
+
+ void ShowResult(byte[] image, string caption = null) => MainThread.BeginInvokeOnMainThread(() =>
+ {
+ OutputImage.Source = ImageSource.FromStream(() => new MemoryStream(image));
+ Caption.Text = string.IsNullOrWhiteSpace(caption) ? string.Empty : caption;
+ });
+
+ void SetBusyState(bool busy)
+ {
+ ExecutionProviderOptions.IsEnabled = !busy;
+ SamplePhotoButton.IsEnabled = !busy;
+ PickPhotoButton.IsEnabled = !busy;
+ TakePhotoButton.IsEnabled = !busy;
+ BusyIndicator.IsEnabled = busy;
+ BusyIndicator.IsRunning = busy;
+ }
+
+ ImageAcquisitionMode GetAcquisitionModeFromText(string tag) => tag switch
+ {
+ nameof(ImageAcquisitionMode.Capture) => ImageAcquisitionMode.Capture,
+ nameof(ImageAcquisitionMode.Pick) => ImageAcquisitionMode.Pick,
+ _ => ImageAcquisitionMode.Sample
+ };
+
+ void AcquireButton_Clicked(object sender, EventArgs e)
+ => AcquireAndAnalyzeImageAsync(GetAcquisitionModeFromText((sender as Button).Text)).ContinueWith((task)
+ => {
+ if (task.IsFaulted) MainThread.BeginInvokeOnMainThread(()
+ => DisplayAlert("Error", task.Exception.Message, "OK"));
+ });
+
+ private void ExecutionProviderOptions_SelectedIndexChanged(object sender, EventArgs e)
+ => UpdateExecutionProviderAsync().ContinueWith((task)
+ => {
+ if (task.IsFaulted) MainThread.BeginInvokeOnMainThread(()
+ => DisplayAlert("Error", task.Exception.Message, "OK"));
+ });
+
+ private void Models_SelectedIndexChanged(object sender, EventArgs e)
+ // make sure EP is in sync
+ => ExecutionProviderOptions_SelectedIndexChanged(null, null);
+}
+
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MauiProgram.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MauiProgram.cs
new file mode 100644
index 000000000..5c4a90106
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MauiProgram.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace MauiVisionSample;
+
+public static class MauiProgram
+{
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder
+ .UseMauiApp()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
+ });
+
+ return builder.Build();
+ }
+}
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MauiVisionSample.csproj b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MauiVisionSample.csproj
new file mode 100644
index 000000000..fd8bcfe91
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/MauiVisionSample.csproj
@@ -0,0 +1,56 @@
+
+
+
+ net6.0-android;net6.0-ios
+ $(TargetFrameworks);net6.0-windows10.0.19041.0
+ Exe
+ MauiVisionSample
+ true
+ true
+ enable
+
+
+ MauiVisionSample
+
+
+ com.microsoft.onnxruntime.mauivisionsample
+ FBA5A6F1-A506-451D-BD28-5D2CF43E3C5A
+
+
+ 1.0
+ 1
+
+ 14.2
+ 21.0
+ 10.0.17763.0
+ 10.0.17763.0
+ en
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetImageProcessor.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetImageProcessor.cs
new file mode 100644
index 000000000..0409afc26
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetImageProcessor.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Microsoft.ML.OnnxRuntime.Tensors;
+using SkiaSharp;
+
+namespace MauiVisionSample
+{
+ public class MobilenetImageProcessor : SkiaSharpImageProcessor
+ {
+ // pre-processing as per https://github.com/onnx/models/blob/main/vision/classification/imagenet_inference.ipynb
+ //
+ // The steps are:
+ // - Resize so the smallest side is 256
+ // - Center crop to 244 x 244
+ // - normalize
+ // - convert to NCHW format with RGB ordering
+ // - N == batch size (1 in this case)
+ // - C == channels - 'r', 'g', 'b' channels
+ // - H == height
+ // - W == width
+ const int RequiredHeight = 224;
+ const int RequiredWidth = 224;
+
+ protected override SKBitmap OnPreprocessSourceImage(SKBitmap sourceImage)
+ {
+ // calculate ratio to reduce the smallest side to 256
+ const int ResizeTo = 256;
+ float ratio = (float)ResizeTo / Math.Min(sourceImage.Width, sourceImage.Height);
+
+ using SKBitmap scaledBitmap = sourceImage.Resize(
+ new SKImageInfo((int)(Math.Ceiling(ratio * sourceImage.Width)),
+ (int)(Math.Ceiling(ratio * sourceImage.Height))),
+ SKFilterQuality.Medium);
+
+ // center crop
+ var horizontalCrop = Math.Max(scaledBitmap.Width - RequiredWidth, 0);
+ var verticalCrop = Math.Max(scaledBitmap.Height - RequiredHeight, 0);
+ var leftOffset = horizontalCrop == 0 ? 0 : horizontalCrop / 2;
+ var topOffset = verticalCrop == 0 ? 0 : verticalCrop / 2;
+
+ var cropRect = SKRectI.Create(new SKPointI(leftOffset, topOffset),
+ new SKSizeI(RequiredWidth, RequiredHeight));
+
+ using SKImage currentImage = SKImage.FromBitmap(scaledBitmap);
+ using SKImage croppedImage = currentImage.Subset(cropRect);
+
+ SKBitmap croppedBitmap = SKBitmap.FromImage(croppedImage);
+ return croppedBitmap;
+ }
+
+ protected override Tensor OnGetTensorForImage(SKBitmap image)
+ {
+ Tensor input = new DenseTensor(new[] { 1, 3, RequiredHeight, RequiredWidth });
+
+ // per-channel normalization values that are applied when converting from a byte to a float
+ var mean = new[] { 0.485f, 0.456f, 0.406f };
+ var stddev = new[] { 0.229f, 0.224f, 0.225f };
+
+ for (int y = 0; y < image.Height; y++)
+ {
+ for (int x = 0; x < image.Width; x++)
+ {
+ // write normalized values to input[N, C, H, W]
+ var pixel = image.GetPixel(x, y);
+ input[0, 0, y, x] = ((pixel.Red / 255f) - mean[0]) / stddev[0];
+ input[0, 1, y, x] = ((pixel.Green / 255f) - mean[1]) / stddev[1];
+ input[0, 2, y, x] = ((pixel.Blue / 255f) - mean[2]) / stddev[2];
+ }
+ }
+
+ return input;
+ }
+ }
+}
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetLabelMap.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetLabelMap.cs
new file mode 100644
index 000000000..a33040d3d
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetLabelMap.cs
@@ -0,0 +1,1010 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace MauiVisionSample
+{
+ public class MobilenetLabelMap
+ {
+ public static readonly string[] Labels = new[] {
+ "tench",
+ "goldfish",
+ "great white shark",
+ "tiger shark",
+ "hammerhead shark",
+ "electric ray",
+ "stingray",
+ "cock",
+ "hen",
+ "ostrich",
+ "brambling",
+ "goldfinch",
+ "house finch",
+ "junco",
+ "indigo bunting",
+ "American robin",
+ "bulbul",
+ "jay",
+ "magpie",
+ "chickadee",
+ "American dipper",
+ "kite",
+ "bald eagle",
+ "vulture",
+ "great grey owl",
+ "fire salamander",
+ "smooth newt",
+ "newt",
+ "spotted salamander",
+ "axolotl",
+ "American bullfrog",
+ "tree frog",
+ "tailed frog",
+ "loggerhead sea turtle",
+ "leatherback sea turtle",
+ "mud turtle",
+ "terrapin",
+ "box turtle",
+ "banded gecko",
+ "green iguana",
+ "Carolina anole",
+ "desert grassland whiptail lizard",
+ "agama",
+ "frilled-necked lizard",
+ "alligator lizard",
+ "Gila monster",
+ "European green lizard",
+ "chameleon",
+ "Komodo dragon",
+ "Nile crocodile",
+ "American alligator",
+ "triceratops",
+ "worm snake",
+ "ring-necked snake",
+ "eastern hog-nosed snake",
+ "smooth green snake",
+ "kingsnake",
+ "garter snake",
+ "water snake",
+ "vine snake",
+ "night snake",
+ "boa constrictor",
+ "African rock python",
+ "Indian cobra",
+ "green mamba",
+ "sea snake",
+ "Saharan horned viper",
+ "eastern diamondback rattlesnake",
+ "sidewinder",
+ "trilobite",
+ "harvestman",
+ "scorpion",
+ "yellow garden spider",
+ "barn spider",
+ "European garden spider",
+ "southern black widow",
+ "tarantula",
+ "wolf spider",
+ "tick",
+ "centipede",
+ "black grouse",
+ "ptarmigan",
+ "ruffed grouse",
+ "prairie grouse",
+ "peacock",
+ "quail",
+ "partridge",
+ "grey parrot",
+ "macaw",
+ "sulphur-crested cockatoo",
+ "lorikeet",
+ "coucal",
+ "bee eater",
+ "hornbill",
+ "hummingbird",
+ "jacamar",
+ "toucan",
+ "duck",
+ "red-breasted merganser",
+ "goose",
+ "black swan",
+ "tusker",
+ "echidna",
+ "platypus",
+ "wallaby",
+ "koala",
+ "wombat",
+ "jellyfish",
+ "sea anemone",
+ "brain coral",
+ "flatworm",
+ "nematode",
+ "conch",
+ "snail",
+ "slug",
+ "sea slug",
+ "chiton",
+ "chambered nautilus",
+ "Dungeness crab",
+ "rock crab",
+ "fiddler crab",
+ "red king crab",
+ "American lobster",
+ "spiny lobster",
+ "crayfish",
+ "hermit crab",
+ "isopod",
+ "white stork",
+ "black stork",
+ "spoonbill",
+ "flamingo",
+ "little blue heron",
+ "great egret",
+ "bittern",
+ "crane (bird)",
+ "limpkin",
+ "common gallinule",
+ "American coot",
+ "bustard",
+ "ruddy turnstone",
+ "dunlin",
+ "common redshank",
+ "dowitcher",
+ "oystercatcher",
+ "pelican",
+ "king penguin",
+ "albatross",
+ "grey whale",
+ "killer whale",
+ "dugong",
+ "sea lion",
+ "Chihuahua",
+ "Japanese Chin",
+ "Maltese",
+ "Pekingese",
+ "Shih Tzu",
+ "King Charles Spaniel",
+ "Papillon",
+ "toy terrier",
+ "Rhodesian Ridgeback",
+ "Afghan Hound",
+ "Basset Hound",
+ "Beagle",
+ "Bloodhound",
+ "Bluetick Coonhound",
+ "Black and Tan Coonhound",
+ "Treeing Walker Coonhound",
+ "English foxhound",
+ "Redbone Coonhound",
+ "borzoi",
+ "Irish Wolfhound",
+ "Italian Greyhound",
+ "Whippet",
+ "Ibizan Hound",
+ "Norwegian Elkhound",
+ "Otterhound",
+ "Saluki",
+ "Scottish Deerhound",
+ "Weimaraner",
+ "Staffordshire Bull Terrier",
+ "American Staffordshire Terrier",
+ "Bedlington Terrier",
+ "Border Terrier",
+ "Kerry Blue Terrier",
+ "Irish Terrier",
+ "Norfolk Terrier",
+ "Norwich Terrier",
+ "Yorkshire Terrier",
+ "Wire Fox Terrier",
+ "Lakeland Terrier",
+ "Sealyham Terrier",
+ "Airedale Terrier",
+ "Cairn Terrier",
+ "Australian Terrier",
+ "Dandie Dinmont Terrier",
+ "Boston Terrier",
+ "Miniature Schnauzer",
+ "Giant Schnauzer",
+ "Standard Schnauzer",
+ "Scottish Terrier",
+ "Tibetan Terrier",
+ "Australian Silky Terrier",
+ "Soft-coated Wheaten Terrier",
+ "West Highland White Terrier",
+ "Lhasa Apso",
+ "Flat-Coated Retriever",
+ "Curly-coated Retriever",
+ "Golden Retriever",
+ "Labrador Retriever",
+ "Chesapeake Bay Retriever",
+ "German Shorthaired Pointer",
+ "Vizsla",
+ "English Setter",
+ "Irish Setter",
+ "Gordon Setter",
+ "Brittany",
+ "Clumber Spaniel",
+ "English Springer Spaniel",
+ "Welsh Springer Spaniel",
+ "Cocker Spaniels",
+ "Sussex Spaniel",
+ "Irish Water Spaniel",
+ "Kuvasz",
+ "Schipperke",
+ "Groenendael",
+ "Malinois",
+ "Briard",
+ "Australian Kelpie",
+ "Komondor",
+ "Old English Sheepdog",
+ "Shetland Sheepdog",
+ "collie",
+ "Border Collie",
+ "Bouvier des Flandres",
+ "Rottweiler",
+ "German Shepherd Dog",
+ "Dobermann",
+ "Miniature Pinscher",
+ "Greater Swiss Mountain Dog",
+ "Bernese Mountain Dog",
+ "Appenzeller Sennenhund",
+ "Entlebucher Sennenhund",
+ "Boxer",
+ "Bullmastiff",
+ "Tibetan Mastiff",
+ "French Bulldog",
+ "Great Dane",
+ "St. Bernard",
+ "husky",
+ "Alaskan Malamute",
+ "Siberian Husky",
+ "Dalmatian",
+ "Affenpinscher",
+ "Basenji",
+ "pug",
+ "Leonberger",
+ "Newfoundland",
+ "Pyrenean Mountain Dog",
+ "Samoyed",
+ "Pomeranian",
+ "Chow Chow",
+ "Keeshond",
+ "Griffon Bruxellois",
+ "Pembroke Welsh Corgi",
+ "Cardigan Welsh Corgi",
+ "Toy Poodle",
+ "Miniature Poodle",
+ "Standard Poodle",
+ "Mexican hairless dog",
+ "grey wolf",
+ "Alaskan tundra wolf",
+ "red wolf",
+ "coyote",
+ "dingo",
+ "dhole",
+ "African wild dog",
+ "hyena",
+ "red fox",
+ "kit fox",
+ "Arctic fox",
+ "grey fox",
+ "tabby cat",
+ "tiger cat",
+ "Persian cat",
+ "Siamese cat",
+ "Egyptian Mau",
+ "cougar",
+ "lynx",
+ "leopard",
+ "snow leopard",
+ "jaguar",
+ "lion",
+ "tiger",
+ "cheetah",
+ "brown bear",
+ "American black bear",
+ "polar bear",
+ "sloth bear",
+ "mongoose",
+ "meerkat",
+ "tiger beetle",
+ "ladybug",
+ "ground beetle",
+ "longhorn beetle",
+ "leaf beetle",
+ "dung beetle",
+ "rhinoceros beetle",
+ "weevil",
+ "fly",
+ "bee",
+ "ant",
+ "grasshopper",
+ "cricket",
+ "stick insect",
+ "cockroach",
+ "mantis",
+ "cicada",
+ "leafhopper",
+ "lacewing",
+ "dragonfly",
+ "damselfly",
+ "red admiral",
+ "ringlet",
+ "monarch butterfly",
+ "small white",
+ "sulphur butterfly",
+ "gossamer-winged butterfly",
+ "starfish",
+ "sea urchin",
+ "sea cucumber",
+ "cottontail rabbit",
+ "hare",
+ "Angora rabbit",
+ "hamster",
+ "porcupine",
+ "fox squirrel",
+ "marmot",
+ "beaver",
+ "guinea pig",
+ "common sorrel",
+ "zebra",
+ "pig",
+ "wild boar",
+ "warthog",
+ "hippopotamus",
+ "ox",
+ "water buffalo",
+ "bison",
+ "ram",
+ "bighorn sheep",
+ "Alpine ibex",
+ "hartebeest",
+ "impala",
+ "gazelle",
+ "dromedary",
+ "llama",
+ "weasel",
+ "mink",
+ "European polecat",
+ "black-footed ferret",
+ "otter",
+ "skunk",
+ "badger",
+ "armadillo",
+ "three-toed sloth",
+ "orangutan",
+ "gorilla",
+ "chimpanzee",
+ "gibbon",
+ "siamang",
+ "guenon",
+ "patas monkey",
+ "baboon",
+ "macaque",
+ "langur",
+ "black-and-white colobus",
+ "proboscis monkey",
+ "marmoset",
+ "white-headed capuchin",
+ "howler monkey",
+ "titi",
+ "Geoffroy's spider monkey",
+ "common squirrel monkey",
+ "ring-tailed lemur",
+ "indri",
+ "Asian elephant",
+ "African bush elephant",
+ "red panda",
+ "giant panda",
+ "snoek",
+ "eel",
+ "coho salmon",
+ "rock beauty",
+ "clownfish",
+ "sturgeon",
+ "garfish",
+ "lionfish",
+ "pufferfish",
+ "abacus",
+ "abaya",
+ "academic gown",
+ "accordion",
+ "acoustic guitar",
+ "aircraft carrier",
+ "airliner",
+ "airship",
+ "altar",
+ "ambulance",
+ "amphibious vehicle",
+ "analog clock",
+ "apiary",
+ "apron",
+ "waste container",
+ "assault rifle",
+ "backpack",
+ "bakery",
+ "balance beam",
+ "balloon",
+ "ballpoint pen",
+ "Band-Aid",
+ "banjo",
+ "baluster",
+ "barbell",
+ "barber chair",
+ "barbershop",
+ "barn",
+ "barometer",
+ "barrel",
+ "wheelbarrow",
+ "baseball",
+ "basketball",
+ "bassinet",
+ "bassoon",
+ "swimming cap",
+ "bath towel",
+ "bathtub",
+ "station wagon",
+ "lighthouse",
+ "beaker",
+ "military cap",
+ "beer bottle",
+ "beer glass",
+ "bell-cot",
+ "bib",
+ "tandem bicycle",
+ "bikini",
+ "ring binder",
+ "binoculars",
+ "birdhouse",
+ "boathouse",
+ "bobsleigh",
+ "bolo tie",
+ "poke bonnet",
+ "bookcase",
+ "bookstore",
+ "bottle cap",
+ "bow",
+ "bow tie",
+ "brass",
+ "bra",
+ "breakwater",
+ "breastplate",
+ "broom",
+ "bucket",
+ "buckle",
+ "bulletproof vest",
+ "high-speed train",
+ "butcher shop",
+ "taxicab",
+ "cauldron",
+ "candle",
+ "cannon",
+ "canoe",
+ "can opener",
+ "cardigan",
+ "car mirror",
+ "carousel",
+ "tool kit",
+ "carton",
+ "car wheel",
+ "automated teller machine",
+ "cassette",
+ "cassette player",
+ "castle",
+ "catamaran",
+ "CD player",
+ "cello",
+ "mobile phone",
+ "chain",
+ "chain-link fence",
+ "chain mail",
+ "chainsaw",
+ "chest",
+ "chiffonier",
+ "chime",
+ "china cabinet",
+ "Christmas stocking",
+ "church",
+ "movie theater",
+ "cleaver",
+ "cliff dwelling",
+ "cloak",
+ "clogs",
+ "cocktail shaker",
+ "coffee mug",
+ "coffeemaker",
+ "coil",
+ "combination lock",
+ "computer keyboard",
+ "confectionery store",
+ "container ship",
+ "convertible",
+ "corkscrew",
+ "cornet",
+ "cowboy boot",
+ "cowboy hat",
+ "cradle",
+ "crane (machine)",
+ "crash helmet",
+ "crate",
+ "infant bed",
+ "Crock Pot",
+ "croquet ball",
+ "crutch",
+ "cuirass",
+ "dam",
+ "desk",
+ "desktop computer",
+ "rotary dial telephone",
+ "diaper",
+ "digital clock",
+ "digital watch",
+ "dining table",
+ "dishcloth",
+ "dishwasher",
+ "disc brake",
+ "dock",
+ "dog sled",
+ "dome",
+ "doormat",
+ "drilling rig",
+ "drum",
+ "drumstick",
+ "dumbbell",
+ "Dutch oven",
+ "electric fan",
+ "electric guitar",
+ "electric locomotive",
+ "entertainment center",
+ "envelope",
+ "espresso machine",
+ "face powder",
+ "feather boa",
+ "filing cabinet",
+ "fireboat",
+ "fire engine",
+ "fire screen sheet",
+ "flagpole",
+ "flute",
+ "folding chair",
+ "football helmet",
+ "forklift",
+ "fountain",
+ "fountain pen",
+ "four-poster bed",
+ "freight car",
+ "French horn",
+ "frying pan",
+ "fur coat",
+ "garbage truck",
+ "gas mask",
+ "gas pump",
+ "goblet",
+ "go-kart",
+ "golf ball",
+ "golf cart",
+ "gondola",
+ "gong",
+ "gown",
+ "grand piano",
+ "greenhouse",
+ "grille",
+ "grocery store",
+ "guillotine",
+ "barrette",
+ "hair spray",
+ "half-track",
+ "hammer",
+ "hamper",
+ "hair dryer",
+ "hand-held computer",
+ "handkerchief",
+ "hard disk drive",
+ "harmonica",
+ "harp",
+ "harvester",
+ "hatchet",
+ "holster",
+ "home theater",
+ "honeycomb",
+ "hook",
+ "hoop skirt",
+ "horizontal bar",
+ "horse-drawn vehicle",
+ "hourglass",
+ "iPod",
+ "clothes iron",
+ "jack-o'-lantern",
+ "jeans",
+ "jeep",
+ "T-shirt",
+ "jigsaw puzzle",
+ "pulled rickshaw",
+ "joystick",
+ "kimono",
+ "knee pad",
+ "knot",
+ "lab coat",
+ "ladle",
+ "lampshade",
+ "laptop computer",
+ "lawn mower",
+ "lens cap",
+ "paper knife",
+ "library",
+ "lifeboat",
+ "lighter",
+ "limousine",
+ "ocean liner",
+ "lipstick",
+ "slip-on shoe",
+ "lotion",
+ "speaker",
+ "loupe",
+ "sawmill",
+ "magnetic compass",
+ "mail bag",
+ "mailbox",
+ "tights",
+ "tank suit",
+ "manhole cover",
+ "maraca",
+ "marimba",
+ "mask",
+ "match",
+ "maypole",
+ "maze",
+ "measuring cup",
+ "medicine chest",
+ "megalith",
+ "microphone",
+ "microwave oven",
+ "military uniform",
+ "milk can",
+ "minibus",
+ "miniskirt",
+ "minivan",
+ "missile",
+ "mitten",
+ "mixing bowl",
+ "mobile home",
+ "Model T",
+ "modem",
+ "monastery",
+ "monitor",
+ "moped",
+ "mortar",
+ "square academic cap",
+ "mosque",
+ "mosquito net",
+ "scooter",
+ "mountain bike",
+ "tent",
+ "computer mouse",
+ "mousetrap",
+ "moving van",
+ "muzzle",
+ "nail",
+ "neck brace",
+ "necklace",
+ "nipple",
+ "notebook computer",
+ "obelisk",
+ "oboe",
+ "ocarina",
+ "odometer",
+ "oil filter",
+ "organ",
+ "oscilloscope",
+ "overskirt",
+ "bullock cart",
+ "oxygen mask",
+ "packet",
+ "paddle",
+ "paddle wheel",
+ "padlock",
+ "paintbrush",
+ "pajamas",
+ "palace",
+ "pan flute",
+ "paper towel",
+ "parachute",
+ "parallel bars",
+ "park bench",
+ "parking meter",
+ "passenger car",
+ "patio",
+ "payphone",
+ "pedestal",
+ "pencil case",
+ "pencil sharpener",
+ "perfume",
+ "Petri dish",
+ "photocopier",
+ "plectrum",
+ "Pickelhaube",
+ "picket fence",
+ "pickup truck",
+ "pier",
+ "piggy bank",
+ "pill bottle",
+ "pillow",
+ "ping-pong ball",
+ "pinwheel",
+ "pirate ship",
+ "pitcher",
+ "hand plane",
+ "planetarium",
+ "plastic bag",
+ "plate rack",
+ "plow",
+ "plunger",
+ "Polaroid camera",
+ "pole",
+ "police van",
+ "poncho",
+ "billiard table",
+ "soda bottle",
+ "pot",
+ "potter's wheel",
+ "power drill",
+ "prayer rug",
+ "printer",
+ "prison",
+ "projectile",
+ "projector",
+ "hockey puck",
+ "punching bag",
+ "purse",
+ "quill",
+ "quilt",
+ "race car",
+ "racket",
+ "radiator",
+ "radio",
+ "radio telescope",
+ "rain barrel",
+ "recreational vehicle",
+ "reel",
+ "reflex camera",
+ "refrigerator",
+ "remote control",
+ "restaurant",
+ "revolver",
+ "rifle",
+ "rocking chair",
+ "rotisserie",
+ "eraser",
+ "rugby ball",
+ "ruler",
+ "running shoe",
+ "safe",
+ "safety pin",
+ "salt shaker",
+ "sandal",
+ "sarong",
+ "saxophone",
+ "scabbard",
+ "weighing scale",
+ "school bus",
+ "schooner",
+ "scoreboard",
+ "CRT screen",
+ "screw",
+ "screwdriver",
+ "seat belt",
+ "sewing machine",
+ "shield",
+ "shoe store",
+ "shoji",
+ "shopping basket",
+ "shopping cart",
+ "shovel",
+ "shower cap",
+ "shower curtain",
+ "ski",
+ "ski mask",
+ "sleeping bag",
+ "slide rule",
+ "sliding door",
+ "slot machine",
+ "snorkel",
+ "snowmobile",
+ "snowplow",
+ "soap dispenser",
+ "soccer ball",
+ "sock",
+ "solar thermal collector",
+ "sombrero",
+ "soup bowl",
+ "space bar",
+ "space heater",
+ "space shuttle",
+ "spatula",
+ "motorboat",
+ "spider web",
+ "spindle",
+ "sports car",
+ "spotlight",
+ "stage",
+ "steam locomotive",
+ "through arch bridge",
+ "steel drum",
+ "stethoscope",
+ "scarf",
+ "stone wall",
+ "stopwatch",
+ "stove",
+ "strainer",
+ "tram",
+ "stretcher",
+ "couch",
+ "stupa",
+ "submarine",
+ "suit",
+ "sundial",
+ "sunglass",
+ "sunglasses",
+ "sunscreen",
+ "suspension bridge",
+ "mop",
+ "sweatshirt",
+ "swimsuit",
+ "swing",
+ "switch",
+ "syringe",
+ "table lamp",
+ "tank",
+ "tape player",
+ "teapot",
+ "teddy bear",
+ "television",
+ "tennis ball",
+ "thatched roof",
+ "front curtain",
+ "thimble",
+ "threshing machine",
+ "throne",
+ "tile roof",
+ "toaster",
+ "tobacco shop",
+ "toilet seat",
+ "torch",
+ "totem pole",
+ "tow truck",
+ "toy store",
+ "tractor",
+ "semi-trailer truck",
+ "tray",
+ "trench coat",
+ "tricycle",
+ "trimaran",
+ "tripod",
+ "triumphal arch",
+ "trolleybus",
+ "trombone",
+ "tub",
+ "turnstile",
+ "typewriter keyboard",
+ "umbrella",
+ "unicycle",
+ "upright piano",
+ "vacuum cleaner",
+ "vase",
+ "vault",
+ "velvet",
+ "vending machine",
+ "vestment",
+ "viaduct",
+ "violin",
+ "volleyball",
+ "waffle iron",
+ "wall clock",
+ "wallet",
+ "wardrobe",
+ "military aircraft",
+ "sink",
+ "washing machine",
+ "water bottle",
+ "water jug",
+ "water tower",
+ "whiskey jug",
+ "whistle",
+ "wig",
+ "window screen",
+ "window shade",
+ "Windsor tie",
+ "wine bottle",
+ "wing",
+ "wok",
+ "wooden spoon",
+ "wool",
+ "split-rail fence",
+ "shipwreck",
+ "yawl",
+ "yurt",
+ "website",
+ "comic book",
+ "crossword",
+ "traffic sign",
+ "traffic light",
+ "dust jacket",
+ "menu",
+ "plate",
+ "guacamole",
+ "consomme",
+ "hot pot",
+ "trifle",
+ "ice cream",
+ "ice pop",
+ "baguette",
+ "bagel",
+ "pretzel",
+ "cheeseburger",
+ "hot dog",
+ "mashed potato",
+ "cabbage",
+ "broccoli",
+ "cauliflower",
+ "zucchini",
+ "spaghetti squash",
+ "acorn squash",
+ "butternut squash",
+ "cucumber",
+ "artichoke",
+ "bell pepper",
+ "cardoon",
+ "mushroom",
+ "Granny Smith",
+ "strawberry",
+ "orange",
+ "lemon",
+ "fig",
+ "pineapple",
+ "banana",
+ "jackfruit",
+ "custard apple",
+ "pomegranate",
+ "hay",
+ "carbonara",
+ "chocolate syrup",
+ "dough",
+ "meatloaf",
+ "pizza",
+ "pot pie",
+ "burrito",
+ "red wine",
+ "espresso",
+ "cup",
+ "eggnog",
+ "alp",
+ "bubble",
+ "cliff",
+ "coral reef",
+ "geyser",
+ "lakeshore",
+ "promontory",
+ "shoal",
+ "seashore",
+ "valley",
+ "volcano",
+ "baseball player",
+ "bridegroom",
+ "scuba diver",
+ "rapeseed",
+ "daisy",
+ "yellow lady's slipper",
+ "corn",
+ "acorn",
+ "rose hip",
+ "horse chestnut seed",
+ "coral fungus",
+ "agaric",
+ "gyromitra",
+ "stinkhorn mushroom",
+ "earth star",
+ "hen-of-the-woods",
+ "bolete",
+ "ear",
+ "toilet paper"};
+ }
+}
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetPrediction.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetPrediction.cs
new file mode 100644
index 000000000..fe6bd7205
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetPrediction.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace MauiVisionSample
+{
+ public class MobilenetPrediction
+ {
+ public string Label { get; set; }
+ public float Confidence { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetSample.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetSample.cs
new file mode 100644
index 000000000..1c35f5937
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Mobilenet/MobilenetSample.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Text;
+using Microsoft.ML.OnnxRuntime;
+using Microsoft.ML.OnnxRuntime.Tensors;
+
+namespace MauiVisionSample
+{
+ // See: https://github.com/onnx/models/tree/main/vision/classification/mobilenet
+ // Model download: https://github.com/onnx/models/blob/main/vision/classification/mobilenet/model/mobilenetv2-12.onnx
+ // NOTE: We use the fp32 version of the model in this example as the int8 version uses internal ONNX Runtime
+ // operators. Due to that, it will not work well with NNAPI or CoreML.
+ public class MobilenetSample : VisionSampleBase
+ {
+ public const string Identifier = "Mobilenet V2";
+ public const string ModelFilename = "mobilenetv2-12.onnx";
+
+ public MobilenetSample()
+ : base(Identifier, ModelFilename) {}
+
+ protected override async Task OnProcessImageAsync(byte[] image)
+ {
+ // Resize and center crop
+ using var preprocessedImage = await Task.Run(() => ImageProcessor.PreprocessSourceImage(image)).ConfigureAwait(false);
+
+ // Convert to Tensor of normalized float RGB data with NCHW ordering
+ var tensor = await Task.Run(() => ImageProcessor.GetTensorForImage(preprocessedImage)).ConfigureAwait(false);
+
+ // Run the model
+ var predictions = await Task.Run(() => GetPredictions(tensor)).ConfigureAwait(false);
+
+ // Get the pre-processed image for display to the user so they can see the actual input to the model
+ var preprocessedImageData = await Task.Run(() => ImageProcessor.GetBytesForBitmap(preprocessedImage)).ConfigureAwait(false);
+
+ var caption = string.Empty;
+
+ if (predictions.Any())
+ {
+ var builder = new StringBuilder();
+
+ if (predictions.Any())
+ {
+ builder.Append($"Top {predictions.Count} predictions: {Environment.NewLine}{Environment.NewLine}");
+ }
+
+ foreach (var prediction in predictions)
+ {
+ builder.Append($"{prediction.Label} ({prediction.Confidence * 100:0.00}%){Environment.NewLine}");
+ }
+
+ caption = builder.ToString();
+ }
+
+ return new ImageProcessingResult(preprocessedImageData, caption);
+ }
+
+ List GetPredictions(Tensor input)
+ {
+ // Setup inputs and outputs
+ var inputs = new List { NamedOnnxValue.CreateFromTensor("input", input) };
+
+ // Run inference
+ using IDisposableReadOnlyCollection results = Session.Run(inputs);
+
+ // Postprocess to get softmax vector
+ IEnumerable output = results.First().AsEnumerable();
+ float sum = output.Sum(x => (float)Math.Exp(x));
+ IEnumerable softmax = output.Select(x => (float)Math.Exp(x) / sum);
+
+ // Extract top 3 predicted classes
+ var top3 = softmax.Select((x, i) => new MobilenetPrediction
+ {
+ Label = MobilenetLabelMap.Labels[i],
+ Confidence = x
+ })
+ .OrderByDescending(x => x.Confidence)
+ .Take(3)
+ .ToList();
+
+ return top3;
+ }
+ }
+}
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Ultraface/UltrafaceImageProcessor.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Ultraface/UltrafaceImageProcessor.cs
new file mode 100644
index 000000000..5c1dd79da
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Ultraface/UltrafaceImageProcessor.cs
@@ -0,0 +1,69 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Microsoft.ML.OnnxRuntime.Tensors;
+using SkiaSharp;
+
+namespace MauiVisionSample
+{
+ public class UltrafaceImageProcessor : SkiaSharpImageProcessor
+ {
+ const int RequiredWidth = 320;
+ const int RequiredHeight = 240;
+
+ protected override SKBitmap OnPreprocessSourceImage(SKBitmap sourceImage)
+ => sourceImage.Resize(new SKImageInfo(RequiredWidth, RequiredHeight), SKFilterQuality.Medium);
+
+ protected override Tensor OnGetTensorForImage(SKBitmap image)
+ {
+ var bytes = image.GetPixelSpan();
+
+ // For Ultraface, the expected input would be 320 x 240 x 4 (in RGBA format)
+ var expectedInputLength = RequiredWidth * RequiredHeight * 4;
+
+ // For the Tensor, we need 3 channels so 320 x 240 x 3 (in RGB format)
+ var expectedOutputLength = RequiredWidth * RequiredHeight * 3;
+
+ if (bytes.Length != expectedInputLength)
+ {
+ throw new Exception($"The parameter {nameof(image)} is an unexpected length. " +
+ $"Expected length is {expectedInputLength}");
+ }
+
+ // The channelData array is expected to be in RGB order with a mean applied i.e. (pixel - 127.0f) / 128.0f
+ float[] channelData = new float[expectedOutputLength];
+
+ // Extract only the desired channel data (don't want the alpha)
+ var expectedChannelLength = expectedOutputLength / 3;
+ var redOffset = expectedChannelLength * 0;
+ var greenOffset = expectedChannelLength * 1;
+ var blueOffset = expectedChannelLength * 2;
+
+ for (int i = 0, i2 = 0; i < bytes.Length; i += 4, i2++)
+ {
+ var r = Convert.ToSingle(bytes[i]);
+ var g = Convert.ToSingle(bytes[i + 1]);
+ var b = Convert.ToSingle(bytes[i + 2]);
+ channelData[i2 + redOffset] = (r - 127.0f) / 128.0f;
+ channelData[i2 + greenOffset] = (g - 127.0f) / 128.0f;
+ channelData[i2 + blueOffset] = (b - 127.0f) / 128.0f;
+ }
+
+ return new DenseTensor(new Memory(channelData),
+ new[] { 1, 3, RequiredHeight, RequiredWidth });
+ }
+
+ protected override void OnApplyPrediction(UltrafacePrediction prediction, SKPaint textPaint,
+ SKPaint rectPaint, SKCanvas canvas)
+ {
+ var text = $"{prediction.Confidence*100:0.00}%";
+ var textBounds = new SKRect();
+ textPaint.MeasureText(text, ref textBounds);
+ canvas.DrawRect(prediction.Box.Xmin, prediction.Box.Ymin,
+ prediction.Box.Xmax - prediction.Box.Xmin, prediction.Box.Ymax - prediction.Box.Ymin,
+ rectPaint);
+ canvas.DrawText(text, prediction.Box.Xmin, prediction.Box.Ymin - textBounds.Height, textPaint);
+ }
+ }
+}
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Ultraface/UltrafacePrediction.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Ultraface/UltrafacePrediction.cs
new file mode 100644
index 000000000..57a82a5a5
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Ultraface/UltrafacePrediction.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace MauiVisionSample
+{
+ public class UltrafacePrediction
+ {
+ public PredictionBox Box { get; set; }
+ public float Confidence { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Ultraface/UltrafaceSample.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Ultraface/UltrafaceSample.cs
new file mode 100644
index 000000000..f62d3076a
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Models/Ultraface/UltrafaceSample.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.ML.OnnxRuntime;
+using Microsoft.ML.OnnxRuntime.Tensors;
+
+namespace MauiVisionSample
+{
+ // See: https://github.com/onnx/models/tree/master/vision/body_analysis/ultraface#model
+ // Model download: https://github.com/onnx/models/blob/master/vision/body_analysis/ultraface/models/version-RFB-320.onnx
+ public class UltrafaceSample : VisionSampleBase
+ {
+ public const string Identifier = "Ultraface";
+ public const string ModelFilename = "Ultraface_version-RFB-320.onnx";
+
+ public UltrafaceSample()
+ : base(Identifier, ModelFilename) {}
+
+ protected override async Task OnProcessImageAsync(byte[] image)
+ {
+ // do initial resize maintaining the aspect ratio so the smallest size is 800. this is arbitrary and
+ // chosen to be a good size to dispay to the user with the results
+ using var sourceImage = await Task.Run(() => ImageProcessor.GetImageFromBytes(image, 800f))
+ .ConfigureAwait(false);
+
+ // do the preprocessing to resize the image to the 320x240 with the model expects.
+ // NOTE: this does not maintain the aspect ratio but works well enough with this particular model.
+ // it may be better in other scenarios to resize and crop to convert the original image to a
+ // 320x240 image.
+ using var preprocessedImage = await Task.Run(() => ImageProcessor.PreprocessSourceImage(image))
+ .ConfigureAwait(false);
+
+ // Convert to Tensor of normalized float RGB data with NCHW ordering
+ var tensor = await Task.Run(() => ImageProcessor.GetTensorForImage(preprocessedImage))
+ .ConfigureAwait(false);
+
+
+ // Run the model
+ var predictions = await Task.Run(() => GetPredictions(tensor, sourceImage.Width, sourceImage.Height))
+ .ConfigureAwait(false);
+
+ // Draw the bounding box for the best prediction on the image from the first resize.
+ var outputImage = await Task.Run(() => ImageProcessor.ApplyPredictionsToImage(predictions, sourceImage))
+ .ConfigureAwait(false);
+
+ return new ImageProcessingResult(outputImage);
+ }
+
+ List GetPredictions(Tensor input, int sourceImageWidth, int sourceImageHeight)
+ {
+ // Setup inputs. Names used must match the input names in the model.
+ var inputs = new List { NamedOnnxValue.CreateFromTensor("input", input) };
+
+ // Run inference
+ using IDisposableReadOnlyCollection results = Session.Run(inputs);
+
+ // Process the results.
+ // First result is the confidence score for each match
+ // Second result are the values to draw a bounding box for each match
+ //
+ // Note that the correct processing is always model specific. Things like the format of the values for
+ // the bounding boxes can vary by model.
+ var resultsArray = results.ToArray();
+ float[] confidences = resultsArray[0].AsEnumerable().ToArray();
+ float[] boxes = resultsArray[1].AsEnumerable().ToArray();
+
+ // Confidences are represented by 2 values - the second is for the face and the first is ignored
+ var scores = confidences.Where((val, index) => index % 2 == 1).ToList();
+
+ // If there were no good matches we return an empty prediction
+ if (!scores.Any(i => i < 0.5))
+ {
+ return new List(); ;
+ }
+
+ // find the best score
+ float highestScore = scores.Max();
+ var indexForHighestScore = scores.IndexOf(highestScore);
+ var boxOffset = indexForHighestScore * 4;
+
+ return new List
+ {
+ new UltrafacePrediction
+ {
+ Confidence = scores[indexForHighestScore],
+ Box = new PredictionBox(
+ boxes[boxOffset + 0] * sourceImageWidth,
+ boxes[boxOffset + 1] * sourceImageHeight,
+ boxes[boxOffset + 2] * sourceImageWidth,
+ boxes[boxOffset + 3] * sourceImageHeight)
+ }
+ };
+ }
+ }
+}
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/AndroidManifest.xml b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 000000000..133da3250
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/MainActivity.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/MainActivity.cs
new file mode 100644
index 000000000..68fafd492
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/MainActivity.cs
@@ -0,0 +1,10 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace MauiVisionSample;
+
+[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
+public class MainActivity : MauiAppCompatActivity
+{
+}
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/MainApplication.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/MainApplication.cs
new file mode 100644
index 000000000..dda3042ec
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/MainApplication.cs
@@ -0,0 +1,22 @@
+using Android.App;
+using Android.Runtime;
+
+// Needed for Picking photo/video
+[assembly: UsesPermission(Android.Manifest.Permission.ReadExternalStorage)]
+
+// Needed for Taking photo/video
+[assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)]
+[assembly: UsesPermission(Android.Manifest.Permission.Camera)]
+
+namespace MauiVisionSample;
+
+[Application]
+public class MainApplication : MauiApplication
+{
+ public MainApplication(IntPtr handle, JniHandleOwnership ownership)
+ : base(handle, ownership)
+ {
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/Resources/values/colors.xml b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 000000000..c04d7492a
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #512BD4
+ #2B0B98
+ #2B0B98
+
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/App.xaml b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/App.xaml
new file mode 100644
index 000000000..592135907
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/App.xaml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/App.xaml.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/App.xaml.cs
new file mode 100644
index 000000000..5bfc2ec3b
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/App.xaml.cs
@@ -0,0 +1,24 @@
+using Microsoft.UI.Xaml;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace MauiVisionSample.WinUI;
+
+///
+/// Provides application-specific behavior to supplement the default Application class.
+///
+public partial class App : MauiWinUIApplication
+{
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/Package.appxmanifest b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 000000000..3ea123bf4
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+ $placeholder$
+ User Name
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/app.manifest b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/app.manifest
new file mode 100644
index 000000000..09feb294e
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/Windows/app.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/iOS/AppDelegate.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 000000000..37e3c2f49
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,9 @@
+using Foundation;
+
+namespace MauiVisionSample;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/iOS/Info.plist b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/iOS/Info.plist
new file mode 100644
index 000000000..b004398d8
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/iOS/Info.plist
@@ -0,0 +1,38 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+ NSCameraUsageDescription
+ This app needs access to the camera to take photos.
+ NSPhotoLibraryAddUsageDescription
+ This app needs access to the photo gallery for picking photos and videos.
+ NSPhotoLibraryUsageDescription
+ This app needs access to photos gallery for picking photos and videos.
+
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/iOS/Program.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/iOS/Program.cs
new file mode 100644
index 000000000..9f37201a9
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Platforms/iOS/Program.cs
@@ -0,0 +1,15 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace MauiVisionSample;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/IImageProcessor.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/IImageProcessor.cs
new file mode 100644
index 000000000..f5c86b711
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/IImageProcessor.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using Microsoft.ML.OnnxRuntime.Tensors;
+
+namespace MauiVisionSample
+{
+ public interface IImageProcessor
+ {
+ // pre-process the image.
+ TImage PreprocessSourceImage(byte[] sourceImage);
+
+ // convert the PreprocessSourceImage output to a Tensor of type TTensor
+ Tensor GetTensorForImage(TImage image);
+
+ // apply the predictions to the image if applicable
+ // e.g. draw the bounding box around an area selected by the model
+ byte[] ApplyPredictionsToImage(IList predictions, TImage image);
+ }
+}
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/ImageProcessingResult.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/ImageProcessingResult.cs
new file mode 100644
index 000000000..5483f3fbd
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/ImageProcessingResult.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace MauiVisionSample
+{
+ public class ImageProcessingResult
+ {
+ public byte[] Image { get; private set; }
+ public string Caption { get; private set; }
+
+ internal ImageProcessingResult(byte[] image, string caption = null)
+ {
+ Image = image;
+ Caption = caption;
+ }
+ }
+}
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/PredictionBox.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/PredictionBox.cs
new file mode 100644
index 000000000..4056868e3
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/PredictionBox.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace MauiVisionSample
+{
+ public class PredictionBox
+ {
+ public float Xmin { get; set; }
+ public float Ymin { get; set; }
+ public float Xmax { get; set; }
+ public float Ymax { get; set; }
+
+ public PredictionBox(float xmin, float ymin, float xmax, float ymax)
+ {
+ Xmin = xmin;
+ Ymin = ymin;
+ Xmax = xmax;
+ Ymax = ymax;
+ }
+ }
+}
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/SkiaSharpImageProcessor.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/SkiaSharpImageProcessor.cs
new file mode 100644
index 000000000..50df6d4ed
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/PrePostProcessing/SkiaSharpImageProcessor.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.ML.OnnxRuntime.Tensors;
+using SkiaSharp;
+
+namespace MauiVisionSample
+{
+ public class SkiaSharpImageProcessor : IImageProcessor
+ {
+ protected virtual SKBitmap OnPreprocessSourceImage(SKBitmap sourceImage) => sourceImage;
+ protected virtual Tensor OnGetTensorForImage(SKBitmap image) => throw new NotImplementedException();
+ protected virtual void OnPrepareToApplyPredictions(SKBitmap image, SKCanvas canvas) { }
+ protected virtual void OnApplyPrediction(TPrediction prediction, SKPaint textPaint, SKPaint rectPaint, SKCanvas canvas) { }
+
+ public byte[] ApplyPredictionsToImage(IList predictions, SKBitmap image)
+ {
+ // Annotate image to reflect predictions and save for viewing
+ using SKSurface surface = SKSurface.Create(new SKImageInfo(image.Width, image.Height));
+ using SKCanvas canvas = surface.Canvas;
+
+ // Normalize paint size based on 800f shortest edge
+ float ratio = 800f / Math.Min(image.Width, image.Height);
+ var textSize = 32 * ratio;
+ var strokeWidth = 2f * ratio;
+
+ using SKPaint textPaint = new SKPaint { TextSize = textSize, Color = SKColors.White };
+ using SKPaint rectPaint = new SKPaint { StrokeWidth = strokeWidth, IsStroke = true, Color = SKColors.Red };
+
+ canvas.DrawBitmap(image, 0, 0);
+
+ OnPrepareToApplyPredictions(image, canvas);
+
+ foreach (var prediction in predictions)
+ OnApplyPrediction(prediction, textPaint, rectPaint, canvas);
+
+ canvas.Flush();
+
+ using var snapshot = surface.Snapshot();
+ using var imageData = snapshot.Encode(SKEncodedImageFormat.Jpeg, 100);
+ byte[] bytes = imageData.ToArray();
+
+ return bytes;
+ }
+
+ public byte[] GetBytesForBitmap(SKBitmap bitmap)
+ {
+ using var image = SKImage.FromBitmap(bitmap);
+ using var data = image.Encode(SKEncodedImageFormat.Jpeg, 100);
+ var bytes = data.ToArray();
+
+ return bytes;
+ }
+
+ public Tensor GetTensorForImage(SKBitmap image)
+ => OnGetTensorForImage(image);
+
+ public Size GetSizeForSourceImage(byte[] sourceImage)
+ {
+ using var image = SKBitmap.Decode(sourceImage);
+ return new Size(image.Width, image.Height);
+ }
+
+ public SKBitmap GetImageFromBytes(byte[] sourceImage, float shortestEdge = -1.0f)
+ {
+ var image = SKBitmap.Decode(sourceImage);
+
+ if (shortestEdge > 0.0)
+ {
+ float ratio = shortestEdge / Math.Min(image.Width, image.Height);
+ image = image.Resize(new SKImageInfo((int)(ratio * image.Width),
+ (int)(ratio * image.Height)),
+ SKFilterQuality.Medium);
+ }
+
+ return image;
+ }
+
+ public SKBitmap PreprocessSourceImage(byte[] sourceImage)
+ {
+ // Read image
+ using var image = SKBitmap.Decode(sourceImage);
+ return OnPreprocessSourceImage(image);
+ }
+ }
+}
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Properties/launchSettings.json b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Properties/launchSettings.json
new file mode 100644
index 000000000..edf8aadcc
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "MsixPackage",
+ "nativeDebugging": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/AppIcon/appicon.svg b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/AppIcon/appicon.svg
new file mode 100644
index 000000000..9d63b6513
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/AppIcon/appicon.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/AppIcon/appiconfg.svg b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 000000000..21dfb25f1
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Fonts/OpenSans-Regular.ttf b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 000000000..36b4b9bc4
Binary files /dev/null and b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Fonts/OpenSans-Semibold.ttf b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 000000000..e7f1e996e
Binary files /dev/null and b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/AboutAssets.txt b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/AboutAssets.txt
new file mode 100644
index 000000000..15d624484
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/AboutAssets.txt
@@ -0,0 +1,15 @@
+Any raw assets you want to be deployed with your application can be placed in
+this directory (and child directories). Deployment of the asset to your application
+is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
+
+
+
+These files will be deployed with you package and will be accessible using Essentials:
+
+ async Task LoadMauiAsset()
+ {
+ using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
+ using var reader = new StreamReader(stream);
+
+ var contents = reader.ReadToEnd();
+ }
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/Ultraface_version-RFB-320.onnx b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/Ultraface_version-RFB-320.onnx
new file mode 100644
index 000000000..3890e76ea
Binary files /dev/null and b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/Ultraface_version-RFB-320.onnx differ
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/mobilenetv2-12.onnx b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/mobilenetv2-12.onnx
new file mode 100644
index 000000000..708233398
Binary files /dev/null and b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/mobilenetv2-12.onnx differ
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/satya.jpg b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/satya.jpg
new file mode 100644
index 000000000..9d215af66
Binary files /dev/null and b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/satya.jpg differ
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/wolves.jpg b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/wolves.jpg
new file mode 100644
index 000000000..8520bffbd
Binary files /dev/null and b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Raw/wolves.jpg differ
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Splash/splash.svg b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Splash/splash.svg
new file mode 100644
index 000000000..21dfb25f1
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Splash/splash.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Styles/Colors.xaml b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Styles/Colors.xaml
new file mode 100644
index 000000000..245758ba1
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Styles/Colors.xaml
@@ -0,0 +1,44 @@
+
+
+
+
+ #512BD4
+ #DFD8F7
+ #2B0B98
+ White
+ Black
+ #E1E1E1
+ #C8C8C8
+ #ACACAC
+ #919191
+ #6E6E6E
+ #404040
+ #212121
+ #141414
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #F7B548
+ #FFD590
+ #FFE5B9
+ #28C2D1
+ #7BDDEF
+ #C3F2F4
+ #3E8EED
+ #72ACF1
+ #A7CBF6
+
+
\ No newline at end of file
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Styles/Styles.xaml b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Styles/Styles.xaml
new file mode 100644
index 000000000..1ec9d55fe
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Resources/Styles/Styles.xaml
@@ -0,0 +1,384 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Utils.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Utils.cs
new file mode 100644
index 000000000..4283b599d
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/Utils.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using SkiaSharp;
+
+namespace MauiVisionSample
+{
+ internal static class Utils
+ {
+ internal static async Task LoadResource(string name)
+ {
+ using Stream fileStream = await FileSystem.Current.OpenAppPackageFileAsync(name);
+ using MemoryStream memoryStream = new MemoryStream();
+ fileStream.CopyTo(memoryStream);
+ return memoryStream.ToArray();
+ }
+
+ public static byte[] HandleOrientation(byte[] image)
+ {
+ using var memoryStream = new MemoryStream(image);
+ using var imageData = SKData.Create(memoryStream);
+ using var codec = SKCodec.Create(imageData);
+ var orientation = codec.EncodedOrigin;
+
+ using var bitmap = SKBitmap.Decode(image);
+ using var adjustedBitmap = AdjustBitmapByOrientation(bitmap, orientation);
+
+ // encode the raw bytes in a known format that SKBitmap.Decode can handle.
+ // doing this makes our APIs a little more flexible as they can take multiple image formats as byte[].
+ // alternatively we could use SKBitmap instead of byte[] to pass the data around and avoid some
+ // SKBitmap.Encode/Decode calls, at the cost of being tightly coupled to the SKBitmap type.
+ using var stream = new MemoryStream();
+ using var wstream = new SKManagedWStream(stream);
+
+ adjustedBitmap.Encode(wstream, SKEncodedImageFormat.Jpeg, 100);
+ var bytes = stream.ToArray();
+
+ return bytes;
+ }
+
+ static SKBitmap AdjustBitmapByOrientation(SKBitmap bitmap, SKEncodedOrigin orientation)
+ {
+ switch (orientation)
+ {
+ case SKEncodedOrigin.BottomRight:
+
+ using (var canvas = new SKCanvas(bitmap))
+ {
+ canvas.RotateDegrees(180, bitmap.Width / 2, bitmap.Height / 2);
+ canvas.DrawBitmap(bitmap.Copy(), 0, 0);
+ }
+
+ return bitmap;
+
+ case SKEncodedOrigin.RightTop:
+
+ using (var rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width))
+ {
+ using (var canvas = new SKCanvas(rotatedBitmap))
+ {
+ canvas.Translate(rotatedBitmap.Width, 0);
+ canvas.RotateDegrees(90);
+ canvas.DrawBitmap(bitmap, 0, 0);
+ }
+
+ rotatedBitmap.CopyTo(bitmap);
+ return bitmap;
+ }
+
+ case SKEncodedOrigin.LeftBottom:
+
+ using (var rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width))
+ {
+ using (var canvas = new SKCanvas(rotatedBitmap))
+ {
+ canvas.Translate(0, rotatedBitmap.Height);
+ canvas.RotateDegrees(270);
+ canvas.DrawBitmap(bitmap, 0, 0);
+ }
+
+ rotatedBitmap.CopyTo(bitmap);
+ return bitmap;
+ }
+
+ default:
+ return bitmap;
+ }
+ }
+ }
+}
+
diff --git a/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/VisionSampleBase.cs b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/VisionSampleBase.cs
new file mode 100644
index 000000000..99ffbde0e
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/MauiVisionSample/VisionSampleBase.cs
@@ -0,0 +1,102 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.ML.OnnxRuntime;
+
+namespace MauiVisionSample
+{
+ public class VisionSampleBase : IVisionSample where TImageProcessor : new()
+ {
+ byte[] _model;
+ string _name;
+ string _modelName;
+ Task _prevAsyncTask;
+ TImageProcessor _imageProcessor;
+ InferenceSession _session;
+ ExecutionProviders _curExecutionProvider;
+
+ public VisionSampleBase(string name, string modelName)
+ {
+ _name = name;
+ _modelName = modelName;
+ _ = InitializeAsync();
+ }
+
+ public string Name => _name;
+ public string ModelName => _modelName;
+ public byte[] Model => _model;
+ public InferenceSession Session => _session;
+ public TImageProcessor ImageProcessor => _imageProcessor ??= new TImageProcessor();
+
+ public async Task UpdateExecutionProviderAsync(ExecutionProviders executionProvider)
+ {
+ // make sure any existing async task completes before we change the session
+ await AwaitLastTaskAsync();
+
+ // creating the inference session can be expensive and should be done as a one-off.
+ // additionally each session uses memory for the model and the infrastructure required to execute it,
+ // and has its own threadpools.
+ _prevAsyncTask =
+ Task.Run(() =>
+ {
+ if (executionProvider == _curExecutionProvider)
+ {
+ return;
+ }
+
+ var options = new SessionOptions();
+
+ if (executionProvider == ExecutionProviders.CPU)
+ {
+ // CPU Execution Provider is always enabled
+ }
+ else if (executionProvider == ExecutionProviders.NNAPI)
+ {
+ options.AppendExecutionProvider_Nnapi();
+ }
+ else if (executionProvider == ExecutionProviders.CoreML)
+ {
+ // add CoreML if the device has an Apple Neural Engine. if it doesn't performance
+ // will most likely be worse than with the CPU Execution Provider.
+ options.AppendExecutionProvider_CoreML(CoreMLFlags.COREML_FLAG_ONLY_ENABLE_DEVICE_WITH_ANE);
+ }
+
+ _session = new InferenceSession(_model, options);
+ });
+ }
+
+ protected virtual Task OnProcessImageAsync(byte[] image) =>
+ throw new NotImplementedException();
+
+ public Task InitializeAsync()
+ {
+ _prevAsyncTask = Initialize();
+ return _prevAsyncTask;
+ }
+
+ public async Task ProcessImageAsync(byte[] image)
+ {
+ await AwaitLastTaskAsync().ConfigureAwait(false);
+
+ return await OnProcessImageAsync(image);
+ }
+
+ async Task AwaitLastTaskAsync()
+ {
+ if (_prevAsyncTask != null)
+ {
+ await _prevAsyncTask.ConfigureAwait(false);
+ _prevAsyncTask = null;
+ }
+ }
+
+ async Task Initialize()
+ {
+ _model = await Utils.LoadResource(_modelName);
+ _session = new InferenceSession(_model); // CPU execution provider is always enabled
+ _curExecutionProvider = ExecutionProviders.CPU;
+ }
+ }
+}
diff --git a/mobile/examples/Maui/MauiVisionSample/readme.md b/mobile/examples/Maui/MauiVisionSample/readme.md
new file mode 100644
index 000000000..92e9c4040
--- /dev/null
+++ b/mobile/examples/Maui/MauiVisionSample/readme.md
@@ -0,0 +1,106 @@
+# ONNX Runtime MAUI Vision Sample
+
+The [MAUI Vision Sample](MauiVisionSample.sln) demonstrates the use of two different vision models from the [ONNX Model Zoo collection](https://github.com/onnx/models/tree/main), by a [MAUI](https://docs.microsoft.com/en-us/dotnet/maui/what-is-maui) app.
+
+## Overview
+The app enables you to take/pick a photo on the device or use a sample image to explore the following models.
+
+### [Mobilenet](https://github.com/onnx/models/tree/main/vision/classification/mobilenet)
+
+Classifies the major object in the image into 1,000 pre-defined classes.
+
+### [Ultraface](https://github.com/onnx/models/tree/main/vision/body_analysis/ultraface)
+
+Lightweight face detection model designed for edge computing devices providing detection boxes and scores for a given image.
+
+This model is included in the repository, but has been updated using the onnxruntime python package tools to:
+- remove unused initializers,
+ - `python -m onnxruntime.tools.optimize_onnx_model --opt_level basic .onnx .onnx`
+- make the initializers constant
+ - requires a script from the ONNX Runtime repository. this can be downloaded from [here](https://github.com/microsoft/onnxruntime/blob/master/tools/python/remove_initializer_from_input.py)
+ - `python remove_initializer_from_input.py --input .onnx --output .onnx`
+
+in order to enable more runtime optimizations to occur.
+
+The sample also demonstrates how to use the default **CPU EP ([Execution Provider](https://onnxruntime.ai/docs/execution-providers))** as well as add a platform-specific execution provider. In this case, [NNAPI](https://onnxruntime.ai/docs/execution-providers/NNAPI-ExecutionProvider.html) for Android and [CoreML](https://onnxruntime.ai/docs/execution-providers/CoreML-ExecutionProvider.html) for iOS.
+
+
+## Building
+
+To build the sample Visual Studio 2022 Preview is currently required, as .net 6 support (which includes MAUI) is only available in that version.
+
+The MAUI workload should be installed. See [here](https://docs.microsoft.com/en-us/dotnet/maui/get-started/first-app) for more information.
+
+If you get any errors during building about missing platform workloads a simple way to resolve is to open a terminal from Visual Studio 2022 Preview (View->Terminal) and run the command `dotnet workload restore MauiVisionSample.sln`
+
+## ONNX Runtime Nuget Packages
+
+There are two ONNX Runtime NuGet packages used.
+ - Microsoft.ML.OnnxRuntime
+ - contains the native libraries for all supported platforms, including various architectures of Windows, Linux, Mac, iOS and Android
+ - Microsoft.ML.OnnxRuntime.Managed
+ - contains the C# bindings to use the native libraries
+
+## ONNX Runtime Usage
+
+The key parts demonstrating the integration of ONNX Runtime are:
+
+### Model
+
+The ONNX models are included in the [Raw](MauiVisionSample/Resources/Raw) resources.
+
+The model bytes are loaded using `OpenAppPackageFileAsync` in `Utils::LoadResource`. Note that it is necessary to copy to a byte[] at runtime as the model may be compressed in the app on platforms such as Android.
+
+### Microsoft.ML.OnnxRuntime.InferenceSession
+
+The `InferenceSession` reads the ONNX model bytes, optimizes the model, and handles model execution.
+
+An `InferenceSession` instance can be used to execute the model multiple times, including concurrently, so should only ever be created once per model.
+
+Creating an `InferenceSession` that uses the CPU Execution Provider is initially done in `VisionSampleBase::Initialize`.
+
+We also demonstrate the steps to enable an additional execution provider (if available for the platform) in `VisionSampleBase::UpdateExecutionProviderAsync`.
+As execution providers must be selected prior to the creation of the `InferenceSession` this necessitates an expensive re-creation of the session in the sample app. Typically you would pre-determine which execution providers you wanted enabled and create the session once with that information.
+
+The CPU Execution Provider will be able to run all models.
+
+In some scenarios it may be beneficial to use the NNAPI Execution Provider on Android, or the CoreML Execution Provider on iOS.
+This is highly dependent on your model, and also the device (particularly for NNAPI), so it is necessary for you to performance test.
+
+## Image acquisition
+
+There are 3 potential ways to acquire the image to process in the sample app.
+
+- If the sample image is selected the bytes from the jpg are loaded from the Raw resources.
+ - see `GetSampleImageAsync` in [MainPage.xaml.cs](MauiVisionSample/MainPage.xaml.cs)
+- If we use the MAUI MediaPicker to select/capture an image we make sure the orientation is correct and convert to a byte[] of jpg format for consistency.
+ - see `TakePhotoAsync` and `PickPhotoAsync` in [MainPage.xaml.cs](MauiVisionSample/MainPage.xaml.cs)
+
+### Pre-processing
+
+Pre-processing the image involves converting it into a Tensor in the same way the input data used during model training was converted. These steps are model specific given they have to match how the individual model was trained.
+
+At a high level there are two steps for the preprocessing:
+
+- First is to convert the byte[] to an SKBitmap using SKBitmap.Decode, and apply any image level changes like resizing and cropping to the bitmap.
+- Second is to convert the bitmap into the Tensor.
+ - for these models that involves converting the byte data in the bitmap to a float value for each R, G and B value in each pixel,
+normalizing those values, and arranging in the NCHW format that ONNX uses.
+ - [MobilenetImageProcessor.cs](MauiVisionSample/Models/Mobilenet/MobilenetImageProcessor.cs) has comments explaining these steps and the NCHW format in more detail.
+
+Each model has an image processor to implement the model specific logic required for these steps.
+We leverage the `SkiaSharpImageProcessor` implementation of the `IImageProcessor` interface for common tasks.
+
+### Model Execution
+
+Once we have converted our input image into a Tensor we can execute the model by calling`InferenceSession::Run` with the Tensor.
+This is done in the implementations of `IVisionSample::GetPredictions`.
+
+### Post-processing
+
+The output for each model is always model specific. The output is first processed into a meaningful format in `IVisionSample::GetPredictions`.
+
+*Mobilenet*: The results involve 1000 scores in the same order as the 1000 labels. We use Softmax to convert the scores to probabilities and return the matching name from the labels and probability for the top 3 matches.
+
+*Ultraface*: The results have confidence scores and bounding boxes for each match. We select the best match, if one is found with a confidence score > 50%, and return the bounding box and confidence score. Additionally, `ApplyPredictionsToImage` will draw the bounding box and score on the input image so we can display the result to the user.
+