Skip to content

Commit

Permalink
refactor: add router to mainwindow and extract folder view
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerardo Lijs committed Mar 15, 2021
1 parent 8b83c18 commit b743f46
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 243 deletions.
3 changes: 2 additions & 1 deletion src/MachineLearning.ObjectDetect.WPF/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MachineLearning.ObjectDetect.WPF"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
StartupUri="Views/MainWindow.xaml">
Startup="Application_Startup"
Exit="Application_Exit">

<Application.Resources>
<ResourceDictionary>
Expand Down
58 changes: 54 additions & 4 deletions src/MachineLearning.ObjectDetect.WPF/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,61 @@
using System.Windows;
using System;
using System.Windows;

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using ReactiveUI;
using Splat;
using Splat.Microsoft.Extensions.DependencyInjection;

namespace MachineLearning.ObjectDetect.WPF
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public IHost AppHost { get; }

public App()
{
// Build AppHost
AppHost = Host
.CreateDefaultBuilder()
.ConfigureServices(ConfigureServices)
.Build();
}

private async void Application_Startup(object sender, StartupEventArgs e)
{
await AppHost.StartAsync();

// Show main window
StartupUri = new Uri("Views/MainWindow.xaml", UriKind.Relative);
}

private async void Application_Exit(object sender, ExitEventArgs e)
{
using (AppHost)
{
await AppHost.StopAsync(TimeSpan.FromSeconds(5));
}
}

private void ConfigureServices(IServiceCollection services)
{
// RxUI uses Splat as its default DI engine but we can instruct it to use Microsoft DI instead
services.UseMicrosoftDependencyResolver();
var resolver = Locator.CurrentMutable;
resolver.InitializeSplat();
resolver.InitializeReactiveUI();

// Manual register ViewModels and Windows
services.AddTransient<Views.MainWindow>();
services.AddTransient<ViewModels.MainWindowViewModel>();

services.AddTransient<ViewModels.FolderViewModel>();
//services.AddTransient<ViewModels.VideoViewModel>();

// Manual register views
services.AddTransient(typeof(IViewFor<ViewModels.FolderViewModel>), typeof(Views.FolderView));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MahApps.Metro" Version="2.4.3" />
<PackageReference Include="MahApps.Metro" Version="2.4.4" />
<PackageReference Include="MahApps.Metro.IconPacks.Material" Version="4.8.0" />
<PackageReference Include="ReactiveUI.WPF" Version="13.1.1" />
<PackageReference Include="ReactiveUI.Fody" Version="12.1.5" />
<PackageReference Include="Microsoft.ML" Version="1.5.4" />
<PackageReference Include="Microsoft.ML.ImageAnalytics" Version="1.5.4" />
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.6.0" />
<PackageReference Include="Microsoft.ML.OnnxTransformer" Version="1.5.4" />
<PackageReference Include="ReactiveUI.WPF" Version="13.2.2" />
<PackageReference Include="ReactiveUI.Fody" Version="13.2.2" />
<PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Microsoft.ML" Version="1.5.5" />
<PackageReference Include="Microsoft.ML.ImageAnalytics" Version="1.5.5" />
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.7.0" />
<PackageReference Include="Microsoft.ML.OnnxTransformer" Version="1.5.5" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="5.0.2" />
</ItemGroup>

Expand Down
121 changes: 121 additions & 0 deletions src/MachineLearning.ObjectDetect.WPF/ViewModels/FolderViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Drawing;
using System.Windows.Media.Imaging;

using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Splat;

using OnnxObjectDetection;

namespace MachineLearning.ObjectDetect.WPF.ViewModels
{
public class FolderViewModel : ReactiveObject, IRoutableViewModel
{
public string UrlPathSegment => "FolderView";
public IScreen HostScreen { get; }
private readonly MainWindowViewModel _mainViewModel;

// Commands
public ReactiveCommand<Unit, Unit> PrevImage { get; }
public ReactiveCommand<Unit, Unit> NextImage { get; }
public ReactiveCommand<Unit, Unit> SelectImageFolder { get; }

[Reactive] public string ImageFolderPath { get; private set; } = string.Empty;
[Reactive] public List<string> ImageList { get; private set; } = new List<string>();
[Reactive] public int ImageCurrentIndex { get; private set; }

[Reactive] public long DetectMilliseconds { get; private set; }
[Reactive] public BitmapSource? DetectImageSource { get; private set; }
public List<BoundingBox> FilteredBoundingBoxes { get; private set; } = new List<BoundingBox>();

// Interactions
public readonly Interaction<Unit, Unit> DrawOverlays = new Interaction<Unit, Unit>();

public FolderViewModel(IScreen? screen = null)
{
HostScreen = screen ?? Locator.Current.GetService<IScreen>();
_mainViewModel = HostScreen as MainWindowViewModel ?? throw new Exception("IScreen must be of type MainWindowViewModel");

// Create command
PrevImage = ReactiveCommand.CreateFromTask(PrevImageImpl);
NextImage = ReactiveCommand.CreateFromTask(NextImageImpl);
SelectImageFolder = ReactiveCommand.CreateFromTask(SelectImageFolderImpl);

// Observables
this.WhenAnyValue(x => x.ImageFolderPath)
.Skip(1)
.Subscribe(folder =>
{
if (string.IsNullOrWhiteSpace(folder)) return;
ImageList = System.IO.Directory.GetFiles(folder).Where(x => x.ToLowerInvariant().EndsWith(".png") || x.ToLowerInvariant().EndsWith(".jpg") || x.ToLowerInvariant().EndsWith(".jpeg") || x.ToLowerInvariant().EndsWith(".bmp")).ToList();
});

// Load image list
ImageFolderPath = System.IO.Path.Combine(Environment.CurrentDirectory, "TestImages");
}

private async Task SelectImageFolderImpl()
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog
{
ShowNewFolderButton = false,
SelectedPath = ImageFolderPath
};

if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
ImageFolderPath = dialog.SelectedPath;
ImageCurrentIndex = 0;
if (ImageList.Count > 0)
{
await NextImage.Execute();
}
}
}

private async Task PrevImageImpl()
{
if (ImageCurrentIndex <= 1) return;
ImageCurrentIndex--;
await LoadAndDetectImage(ImageList[ImageCurrentIndex - 1]);
}

private async Task NextImageImpl()
{
if (ImageList.Count == ImageCurrentIndex) return;
ImageCurrentIndex++;
await LoadAndDetectImage(ImageList[ImageCurrentIndex - 1]);
}

private async Task DetectImage(Bitmap bitmap)
{
var imageInputData = new ImageInputData { Image = bitmap };

var sw = System.Diagnostics.Stopwatch.StartNew();

var labels = _mainViewModel.CustomVisionPredictionEngine.Predict(imageInputData).PredictedLabels;
var boundingBoxes = _mainViewModel.OutputParser.ParseOutputs(labels);
FilteredBoundingBoxes = _mainViewModel.OutputParser.FilterBoundingBoxes(boundingBoxes, 5, 0.5f);

// Time spent for detection by ML.NET
DetectMilliseconds = sw.ElapsedMilliseconds;

await DrawOverlays.Handle(Unit.Default);
}

private async Task LoadAndDetectImage(string filename)
{
var bitmapImage = new Bitmap(filename);
DetectImageSource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(bitmapImage.GetHbitmap(), IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
await DetectImage(bitmapImage);
}
}
}
107 changes: 13 additions & 94 deletions src/MachineLearning.ObjectDetect.WPF/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
using System.Reactive.Linq;
using System.Threading.Tasks;

using System.Drawing;
using System.Windows.Media.Imaging;

using Splat;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;

Expand All @@ -16,112 +14,33 @@

namespace MachineLearning.ObjectDetect.WPF.ViewModels
{
public class MainWindowViewModel : ReactiveObject
public class MainWindowViewModel : ReactiveObject, IScreen
{
public RoutingState Router { get; }

// TODO: Change this to UI select folder and change model in runtime
private readonly OnnxOutputParser outputParser;
private readonly PredictionEngine<ImageInputData, CustomVisionPrediction> customVisionPredictionEngine;
public OnnxOutputParser OutputParser { get; }
public PredictionEngine<ImageInputData, CustomVisionPrediction> CustomVisionPredictionEngine { get; }
private readonly string modelsDirectory = System.IO.Path.Combine(Environment.CurrentDirectory, @"OnnxModels");

// Commands
public ReactiveCommand<Unit, Unit> PrevImage { get; }
public ReactiveCommand<Unit, Unit> NextImage { get; }
public ReactiveCommand<Unit, Unit> SelectImageFolder { get; }

[Reactive] public string ImageFolderPath { get; private set; } = string.Empty;
[Reactive] public List<string> ImageList { get; private set; } = new List<string>();
[Reactive] public int ImageCurrentIndex { get; private set; }

[Reactive] public long DetectMilliseconds { get; private set; }
[Reactive] public BitmapSource? DetectImageSource { get; private set; }
public List<BoundingBox> FilteredBoundingBoxes { get; private set; } = new List<BoundingBox>();

// Interactions
public readonly Interaction<Unit, Unit> DrawOverlays = new Interaction<Unit, Unit>();

public MainWindowViewModel()
{
// Create command
PrevImage = ReactiveCommand.CreateFromTask(PrevImageImpl);
NextImage = ReactiveCommand.CreateFromTask(NextImageImpl);
SelectImageFolder = ReactiveCommand.CreateFromTask(SelectImageFolderImpl);
Locator.CurrentMutable.RegisterConstant(this,typeof(IScreen));

// Load Onnx model
var customVisionExport = System.IO.Directory.GetFiles(modelsDirectory, "*.zip").FirstOrDefault();

var customVisionModel = new CustomVisionModel(customVisionExport);
var modelConfigurator = new OnnxModelConfigurator(customVisionModel);

outputParser = new OnnxOutputParser(customVisionModel);
customVisionPredictionEngine = modelConfigurator.GetMlNetPredictionEngine<CustomVisionPrediction>();

// Observables
this.WhenAnyValue(x => x.ImageFolderPath)
.Skip(1)
.Subscribe(folder =>
{
if (string.IsNullOrWhiteSpace(folder)) return;
ImageList = System.IO.Directory.GetFiles(folder).Where(x => x.ToLowerInvariant().EndsWith(".png") || x.ToLowerInvariant().EndsWith(".jpg") || x.ToLowerInvariant().EndsWith(".jpeg") || x.ToLowerInvariant().EndsWith(".bmp")).ToList();
});

// Load image list
ImageFolderPath = System.IO.Path.Combine(Environment.CurrentDirectory, "TestImages");
}

private async Task SelectImageFolderImpl()
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog
{
ShowNewFolderButton = false,
SelectedPath = ImageFolderPath
};

if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
ImageFolderPath = dialog.SelectedPath;
ImageCurrentIndex = 0;
if (ImageList.Count > 0)
{
await NextImage.Execute();
}
}
}

private async Task PrevImageImpl()
{
if (ImageCurrentIndex <= 1) return;
ImageCurrentIndex--;
await LoadAndDetectImage(ImageList[ImageCurrentIndex - 1]);
}
OutputParser = new OnnxOutputParser(customVisionModel);
CustomVisionPredictionEngine = modelConfigurator.GetMlNetPredictionEngine<CustomVisionPrediction>();

private async Task NextImageImpl()
{
if (ImageList.Count == ImageCurrentIndex) return;
ImageCurrentIndex++;
await LoadAndDetectImage(ImageList[ImageCurrentIndex - 1]);
}

async Task DetectImage(Bitmap bitmap)
{
var imageInputData = new ImageInputData { Image = bitmap };
// Initialize the Router.
Router = new RoutingState();

var sw = System.Diagnostics.Stopwatch.StartNew();

var labels = customVisionPredictionEngine.Predict(imageInputData).PredictedLabels;
var boundingBoxes = outputParser.ParseOutputs(labels);
FilteredBoundingBoxes = outputParser.FilterBoundingBoxes(boundingBoxes, 5, 0.5f);

// Time spent for detection by ML.NET
DetectMilliseconds = sw.ElapsedMilliseconds;

await DrawOverlays.Handle(Unit.Default);
}

private async Task LoadAndDetectImage(string filename)
{
var bitmapImage = new Bitmap(filename);
DetectImageSource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(bitmapImage.GetHbitmap(), IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
await DetectImage(bitmapImage);
// Start with New Capture content
Router.Navigate.Execute(Locator.Current.GetService<FolderViewModel>());
}
}
}
Loading

0 comments on commit b743f46

Please sign in to comment.