Skip to content

Commit

Permalink
Merge pull request gerardo-lijs#1 from gerardo-lijs/feature/select-model
Browse files Browse the repository at this point in the history
feat: select model option and webcam view
  • Loading branch information
gerardo-lijs authored Mar 17, 2021
2 parents 8b83c18 + 2f17c10 commit 2e48f55
Show file tree
Hide file tree
Showing 22 changed files with 1,676 additions and 224 deletions.
7 changes: 5 additions & 2 deletions 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 All @@ -24,8 +25,10 @@
</ResourceDictionary>

<!-- Accent and AppTheme setting -->
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
<!--<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Dark.Blue.xaml" />-->

<!-- Custom styles -->
<ResourceDictionary Source="pack://application:,,,/MachineLearning.ObjectDetect.WPF;component/ControlStyles/ToggleSwitch.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
Expand Down
75 changes: 71 additions & 4 deletions src/MachineLearning.ObjectDetect.WPF/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,78 @@
using System.Windows;
using System;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

using ControlzEx.Theming;
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();

// Theme
var useWindowsTheme = false;
if (useWindowsTheme)
{
// Use Windows theme
ThemeManager.Current.ThemeSyncMode = ThemeSyncMode.SyncWithAppMode;
ThemeManager.Current.SyncTheme();
}
else
{
// Default Dark
ThemeManager.Current.ChangeTheme(this, "Dark.Blue");
}

// 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.SelectViewModel>();
services.AddTransient<ViewModels.FolderViewModel>();
services.AddTransient<ViewModels.WebcamViewModel>();

// Manual register views
services.AddTransient(typeof(IViewFor<ViewModels.SelectViewModel>), typeof(Views.SelectView));
services.AddTransient(typeof(IViewFor<ViewModels.FolderViewModel>), typeof(Views.FolderView));
services.AddTransient(typeof(IViewFor<ViewModels.WebcamViewModel>), typeof(Views.WebcamView));
}
}
}
495 changes: 495 additions & 0 deletions src/MachineLearning.ObjectDetect.WPF/ControlStyles/ToggleSwitch.xaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/MachineLearning.ObjectDetect.WPF/FodyWeavers.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ReactiveUI />
<PropertyChanged />
</Weavers>
49 changes: 49 additions & 0 deletions src/MachineLearning.ObjectDetect.WPF/FodyWeavers.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,55 @@
<xs:complexType>
<xs:all>
<xs:element name="ReactiveUI" minOccurs="0" maxOccurs="1" type="xs:anyType" />
<xs:element name="PropertyChanged" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="InjectOnPropertyNameChanged" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if the On_PropertyName_Changed feature is enabled.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="TriggerDependentProperties" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if the Dependent properties feature is enabled.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="EnableIsChangedProperty" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if the IsChanged property feature is enabled.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="EventInvokerNames" type="xs:string">
<xs:annotation>
<xs:documentation>Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="CheckForEquality" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="CheckForEqualityUsingBaseEquals" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if equality checks should use the Equals method resolved from the base class.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UseStaticEqualsFromBase" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to control if equality checks should use the static Equals method resolved from the base class.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="SuppressWarnings" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to turn off build warnings from this weaver.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="SuppressOnPropertyNameChangedWarning" type="xs:boolean">
<xs:annotation>
<xs:documentation>Used to turn off build warnings about mismatched On_PropertyName_Changed methods.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,23 @@
</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="DirectShowLib.Standard" Version="2.1.0" />
<PackageReference Include="OpenCvSharp4.Windows" Version="4.5.1.20210210" />
<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" />
<PackageReference Include="PropertyChanged.Fody" Version="3.3.2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand All @@ -28,6 +36,9 @@
<None Update="OnnxModels\facemask.ONNX.zip">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="OnnxModels\TinyYolo2_model.onnx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestImages\00_maksssksksss598.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down Expand Up @@ -72,4 +83,8 @@
</None>
</ItemGroup>

<ItemGroup>
<Folder Include="ControlStyles\" />
</ItemGroup>

</Project>
Binary file not shown.
148 changes: 148 additions & 0 deletions src/MachineLearning.ObjectDetect.WPF/Services/CameraOpenCv.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using OpenCvSharp;

namespace MachineLearning.ObjectDetect.WPF.Services
{
public static class CameraOpenCvEvents
{
public delegate void GrabContinuousStartedEventHandler();
public delegate void GrabContinuousStoppedEventHandler();
}

public sealed class CameraOpenCv : IDisposable, INotifyPropertyChanged
{
#pragma warning disable CS0067 // Event used by Fody
public event PropertyChangedEventHandler? PropertyChanged;
#pragma warning restore CS0067 // Event used by Fody
public record CameraDevice(int CameraIndex, string Name, string DeviceId);
public record ImageGrabbedData(Mat image, double CurrentFPS);
/// <summary>
/// Enumerates the connected cameras.
/// </summary>
public static IEnumerable<CameraDevice> EnumerateAllConnectedCameras() =>
DirectShowLib.DsDevice.GetDevicesOfCat(DirectShowLib.FilterCategory.VideoInputDevice).Select((x, cameraIndex) => new CameraOpenCv.CameraDevice(cameraIndex++, x.Name, x.DevicePath));

private CancellationTokenSource? _cts;
private readonly System.Diagnostics.Stopwatch _fpsStopWatch = new System.Diagnostics.Stopwatch();

public ISubject<ImageGrabbedData> ImageGrabbed { get; } = new Subject<ImageGrabbedData>();
public event CameraOpenCvEvents.GrabContinuousStartedEventHandler? GrabContinuousStarted;
public event CameraOpenCvEvents.GrabContinuousStoppedEventHandler? GrabContinuousStopped;
public bool IsGrabbing { get; private set; }

/// <summary>
/// Frame rate used to display video.
/// </summary>
public int MaxDisplayFrameRate { get; set; } = 30;

public bool FlipImageY { get; set; }
public bool FlipImageX { get; set; }
public double CurrentFPS { get; private set; }

public async Task GrabContinuous_Start(int cameraIndex)
{
_cts = new CancellationTokenSource();
await Task.Run(() => GrabContinuous_Proc(cameraIndex, _cts.Token), _cts.Token);
}

private async Task GrabContinuous_Proc(int cameraIndex, CancellationToken cancellationToken)
{
// FPS delay
var fpsMilliseconds = 1000 / MaxDisplayFrameRate;

// Init capture
using var videoCapture = new VideoCapture(cameraIndex);
videoCapture.Open(cameraIndex);
if (!videoCapture.IsOpened()) throw new Exception("Could not open camera.");

// TODO: Refactor this to Thread or in Caller
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
IsGrabbing = true;
});
GrabContinuousStarted?.Invoke();

var fpsCounter = new List<double>();

// Grab
using var frame = new Mat();
while (!cancellationToken.IsCancellationRequested)
{
// Reduce the number of displayed images to a reasonable amount if the camera is acquiring images very fast.
if (!_fpsStopWatch.IsRunning || _fpsStopWatch.ElapsedMilliseconds > fpsMilliseconds)
{
// Display FPS counter
if (_fpsStopWatch.IsRunning)
{
fpsCounter.Add(1000 / _fpsStopWatch.ElapsedMilliseconds);
if (fpsCounter.Count > MaxDisplayFrameRate / 2)
{
CurrentFPS = fpsCounter.Average();
fpsCounter.Clear();
}
}

_fpsStopWatch.Restart();

// Get frame
videoCapture.Read(frame);

if (!frame.Empty())
{
// Optional flip
Mat workFrame = FlipImageY ? frame.Flip(FlipMode.Y) : frame;
workFrame = FlipImageX ? workFrame.Flip(FlipMode.X) : workFrame;

if (!cancellationToken.IsCancellationRequested)
{
ImageGrabbed.OnNext(new ImageGrabbedData(workFrame, CurrentFPS));
}
}
}
else
{
// Display frame rate speed to get desired display frame rate. We use half the expected time to consider time spent executing other code
var fpsDelay = (fpsMilliseconds / 2) - (int)_fpsStopWatch.ElapsedMilliseconds;
if (fpsDelay > 0) await Task.Delay(fpsDelay, CancellationToken.None);
}
}

videoCapture.Release();
// TODO: Refactor this to Thread or in Caller
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
IsGrabbing = false;
CurrentFPS = 0;
});
GrabContinuousStopped?.Invoke();
}

public async Task GrabContinuous_Stop()
{
// Check
if (_cts is null) return;

// If "Dispose" gets called before Stop
if (_cts.IsCancellationRequested) return;

_cts.Cancel();

// Wait for grab to finish - TODO: Refactor this!
await Task.Delay(250);
}

public void Dispose()
{
_cts?.Cancel();
}
}
}
Loading

0 comments on commit 2e48f55

Please sign in to comment.