Skip to content

Commit 2e48f55

Browse files
authored
Merge pull request gerardo-lijs#1 from gerardo-lijs/feature/select-model
feat: select model option and webcam view
2 parents 8b83c18 + 2f17c10 commit 2e48f55

22 files changed

+1676
-224
lines changed

src/MachineLearning.ObjectDetect.WPF/App.xaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
55
xmlns:local="clr-namespace:MachineLearning.ObjectDetect.WPF"
66
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
7-
StartupUri="Views/MainWindow.xaml">
7+
Startup="Application_Startup"
8+
Exit="Application_Exit">
89

910
<Application.Resources>
1011
<ResourceDictionary>
@@ -24,8 +25,10 @@
2425
</ResourceDictionary>
2526

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

30+
<!-- Custom styles -->
31+
<ResourceDictionary Source="pack://application:,,,/MachineLearning.ObjectDetect.WPF;component/ControlStyles/ToggleSwitch.xaml" />
2932
</ResourceDictionary.MergedDictionaries>
3033
</ResourceDictionary>
3134
</Application.Resources>
Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,78 @@
1-
using System.Windows;
1+
using System;
2+
using System.Windows;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Hosting;
5+
6+
using ControlzEx.Theming;
7+
using ReactiveUI;
8+
using Splat;
9+
using Splat.Microsoft.Extensions.DependencyInjection;
210

311
namespace MachineLearning.ObjectDetect.WPF
412
{
5-
/// <summary>
6-
/// Interaction logic for App.xaml
7-
/// </summary>
813
public partial class App : Application
914
{
15+
public IHost AppHost { get; }
16+
17+
public App()
18+
{
19+
// Build AppHost
20+
AppHost = Host
21+
.CreateDefaultBuilder()
22+
.ConfigureServices(ConfigureServices)
23+
.Build();
24+
}
25+
26+
private async void Application_Startup(object sender, StartupEventArgs e)
27+
{
28+
await AppHost.StartAsync();
29+
30+
// Theme
31+
var useWindowsTheme = false;
32+
if (useWindowsTheme)
33+
{
34+
// Use Windows theme
35+
ThemeManager.Current.ThemeSyncMode = ThemeSyncMode.SyncWithAppMode;
36+
ThemeManager.Current.SyncTheme();
37+
}
38+
else
39+
{
40+
// Default Dark
41+
ThemeManager.Current.ChangeTheme(this, "Dark.Blue");
42+
}
43+
44+
// Show main window
45+
StartupUri = new Uri("Views/MainWindow.xaml", UriKind.Relative);
46+
}
47+
48+
private async void Application_Exit(object sender, ExitEventArgs e)
49+
{
50+
using (AppHost)
51+
{
52+
await AppHost.StopAsync(TimeSpan.FromSeconds(5));
53+
}
54+
}
55+
56+
private void ConfigureServices(IServiceCollection services)
57+
{
58+
// RxUI uses Splat as its default DI engine but we can instruct it to use Microsoft DI instead
59+
services.UseMicrosoftDependencyResolver();
60+
var resolver = Locator.CurrentMutable;
61+
resolver.InitializeSplat();
62+
resolver.InitializeReactiveUI();
63+
64+
// Manual register ViewModels and Windows
65+
services.AddTransient<Views.MainWindow>();
66+
services.AddTransient<ViewModels.MainWindowViewModel>();
67+
68+
services.AddTransient<ViewModels.SelectViewModel>();
69+
services.AddTransient<ViewModels.FolderViewModel>();
70+
services.AddTransient<ViewModels.WebcamViewModel>();
71+
72+
// Manual register views
73+
services.AddTransient(typeof(IViewFor<ViewModels.SelectViewModel>), typeof(Views.SelectView));
74+
services.AddTransient(typeof(IViewFor<ViewModels.FolderViewModel>), typeof(Views.FolderView));
75+
services.AddTransient(typeof(IViewFor<ViewModels.WebcamViewModel>), typeof(Views.WebcamView));
76+
}
1077
}
1178
}

src/MachineLearning.ObjectDetect.WPF/ControlStyles/ToggleSwitch.xaml

Lines changed: 495 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
22
<ReactiveUI />
3+
<PropertyChanged />
34
</Weavers>

src/MachineLearning.ObjectDetect.WPF/FodyWeavers.xsd

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,55 @@
55
<xs:complexType>
66
<xs:all>
77
<xs:element name="ReactiveUI" minOccurs="0" maxOccurs="1" type="xs:anyType" />
8+
<xs:element name="PropertyChanged" minOccurs="0" maxOccurs="1">
9+
<xs:complexType>
10+
<xs:attribute name="InjectOnPropertyNameChanged" type="xs:boolean">
11+
<xs:annotation>
12+
<xs:documentation>Used to control if the On_PropertyName_Changed feature is enabled.</xs:documentation>
13+
</xs:annotation>
14+
</xs:attribute>
15+
<xs:attribute name="TriggerDependentProperties" type="xs:boolean">
16+
<xs:annotation>
17+
<xs:documentation>Used to control if the Dependent properties feature is enabled.</xs:documentation>
18+
</xs:annotation>
19+
</xs:attribute>
20+
<xs:attribute name="EnableIsChangedProperty" type="xs:boolean">
21+
<xs:annotation>
22+
<xs:documentation>Used to control if the IsChanged property feature is enabled.</xs:documentation>
23+
</xs:annotation>
24+
</xs:attribute>
25+
<xs:attribute name="EventInvokerNames" type="xs:string">
26+
<xs:annotation>
27+
<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>
28+
</xs:annotation>
29+
</xs:attribute>
30+
<xs:attribute name="CheckForEquality" type="xs:boolean">
31+
<xs:annotation>
32+
<xs:documentation>Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.</xs:documentation>
33+
</xs:annotation>
34+
</xs:attribute>
35+
<xs:attribute name="CheckForEqualityUsingBaseEquals" type="xs:boolean">
36+
<xs:annotation>
37+
<xs:documentation>Used to control if equality checks should use the Equals method resolved from the base class.</xs:documentation>
38+
</xs:annotation>
39+
</xs:attribute>
40+
<xs:attribute name="UseStaticEqualsFromBase" type="xs:boolean">
41+
<xs:annotation>
42+
<xs:documentation>Used to control if equality checks should use the static Equals method resolved from the base class.</xs:documentation>
43+
</xs:annotation>
44+
</xs:attribute>
45+
<xs:attribute name="SuppressWarnings" type="xs:boolean">
46+
<xs:annotation>
47+
<xs:documentation>Used to turn off build warnings from this weaver.</xs:documentation>
48+
</xs:annotation>
49+
</xs:attribute>
50+
<xs:attribute name="SuppressOnPropertyNameChangedWarning" type="xs:boolean">
51+
<xs:annotation>
52+
<xs:documentation>Used to turn off build warnings about mismatched On_PropertyName_Changed methods.</xs:documentation>
53+
</xs:annotation>
54+
</xs:attribute>
55+
</xs:complexType>
56+
</xs:element>
857
</xs:all>
958
<xs:attribute name="VerifyAssembly" type="xs:boolean">
1059
<xs:annotation>

src/MachineLearning.ObjectDetect.WPF/MachineLearning.ObjectDetect.WPF.csproj

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,23 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="MahApps.Metro" Version="2.4.3" />
12+
<PackageReference Include="MahApps.Metro" Version="2.4.4" />
1313
<PackageReference Include="MahApps.Metro.IconPacks.Material" Version="4.8.0" />
14-
<PackageReference Include="ReactiveUI.WPF" Version="13.1.1" />
15-
<PackageReference Include="ReactiveUI.Fody" Version="12.1.5" />
16-
<PackageReference Include="Microsoft.ML" Version="1.5.4" />
17-
<PackageReference Include="Microsoft.ML.ImageAnalytics" Version="1.5.4" />
18-
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.6.0" />
19-
<PackageReference Include="Microsoft.ML.OnnxTransformer" Version="1.5.4" />
14+
<PackageReference Include="DirectShowLib.Standard" Version="2.1.0" />
15+
<PackageReference Include="OpenCvSharp4.Windows" Version="4.5.1.20210210" />
16+
<PackageReference Include="ReactiveUI.WPF" Version="13.2.2" />
17+
<PackageReference Include="ReactiveUI.Fody" Version="13.2.2" />
18+
<PackageReference Include="Splat.Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
19+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
20+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
21+
<PackageReference Include="Microsoft.ML" Version="1.5.5" />
22+
<PackageReference Include="Microsoft.ML.ImageAnalytics" Version="1.5.5" />
23+
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.7.0" />
24+
<PackageReference Include="Microsoft.ML.OnnxTransformer" Version="1.5.5" />
2025
<PackageReference Include="Microsoft.Windows.Compatibility" Version="5.0.2" />
26+
<PackageReference Include="PropertyChanged.Fody" Version="3.3.2">
27+
<PrivateAssets>all</PrivateAssets>
28+
</PackageReference>
2129
</ItemGroup>
2230

2331
<ItemGroup>
@@ -28,6 +36,9 @@
2836
<None Update="OnnxModels\facemask.ONNX.zip">
2937
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3038
</None>
39+
<None Update="OnnxModels\TinyYolo2_model.onnx">
40+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
41+
</None>
3142
<None Update="TestImages\00_maksssksksss598.png">
3243
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3344
</None>
@@ -72,4 +83,8 @@
7283
</None>
7384
</ItemGroup>
7485

86+
<ItemGroup>
87+
<Folder Include="ControlStyles\" />
88+
</ItemGroup>
89+
7590
</Project>
Binary file not shown.
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel;
4+
using System.Linq;
5+
using System.Reactive.Linq;
6+
using System.Reactive.Subjects;
7+
using System.Text;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
using OpenCvSharp;
12+
13+
namespace MachineLearning.ObjectDetect.WPF.Services
14+
{
15+
public static class CameraOpenCvEvents
16+
{
17+
public delegate void GrabContinuousStartedEventHandler();
18+
public delegate void GrabContinuousStoppedEventHandler();
19+
}
20+
21+
public sealed class CameraOpenCv : IDisposable, INotifyPropertyChanged
22+
{
23+
#pragma warning disable CS0067 // Event used by Fody
24+
public event PropertyChangedEventHandler? PropertyChanged;
25+
#pragma warning restore CS0067 // Event used by Fody
26+
public record CameraDevice(int CameraIndex, string Name, string DeviceId);
27+
public record ImageGrabbedData(Mat image, double CurrentFPS);
28+
/// <summary>
29+
/// Enumerates the connected cameras.
30+
/// </summary>
31+
public static IEnumerable<CameraDevice> EnumerateAllConnectedCameras() =>
32+
DirectShowLib.DsDevice.GetDevicesOfCat(DirectShowLib.FilterCategory.VideoInputDevice).Select((x, cameraIndex) => new CameraOpenCv.CameraDevice(cameraIndex++, x.Name, x.DevicePath));
33+
34+
private CancellationTokenSource? _cts;
35+
private readonly System.Diagnostics.Stopwatch _fpsStopWatch = new System.Diagnostics.Stopwatch();
36+
37+
public ISubject<ImageGrabbedData> ImageGrabbed { get; } = new Subject<ImageGrabbedData>();
38+
public event CameraOpenCvEvents.GrabContinuousStartedEventHandler? GrabContinuousStarted;
39+
public event CameraOpenCvEvents.GrabContinuousStoppedEventHandler? GrabContinuousStopped;
40+
public bool IsGrabbing { get; private set; }
41+
42+
/// <summary>
43+
/// Frame rate used to display video.
44+
/// </summary>
45+
public int MaxDisplayFrameRate { get; set; } = 30;
46+
47+
public bool FlipImageY { get; set; }
48+
public bool FlipImageX { get; set; }
49+
public double CurrentFPS { get; private set; }
50+
51+
public async Task GrabContinuous_Start(int cameraIndex)
52+
{
53+
_cts = new CancellationTokenSource();
54+
await Task.Run(() => GrabContinuous_Proc(cameraIndex, _cts.Token), _cts.Token);
55+
}
56+
57+
private async Task GrabContinuous_Proc(int cameraIndex, CancellationToken cancellationToken)
58+
{
59+
// FPS delay
60+
var fpsMilliseconds = 1000 / MaxDisplayFrameRate;
61+
62+
// Init capture
63+
using var videoCapture = new VideoCapture(cameraIndex);
64+
videoCapture.Open(cameraIndex);
65+
if (!videoCapture.IsOpened()) throw new Exception("Could not open camera.");
66+
67+
// TODO: Refactor this to Thread or in Caller
68+
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
69+
{
70+
IsGrabbing = true;
71+
});
72+
GrabContinuousStarted?.Invoke();
73+
74+
var fpsCounter = new List<double>();
75+
76+
// Grab
77+
using var frame = new Mat();
78+
while (!cancellationToken.IsCancellationRequested)
79+
{
80+
// Reduce the number of displayed images to a reasonable amount if the camera is acquiring images very fast.
81+
if (!_fpsStopWatch.IsRunning || _fpsStopWatch.ElapsedMilliseconds > fpsMilliseconds)
82+
{
83+
// Display FPS counter
84+
if (_fpsStopWatch.IsRunning)
85+
{
86+
fpsCounter.Add(1000 / _fpsStopWatch.ElapsedMilliseconds);
87+
if (fpsCounter.Count > MaxDisplayFrameRate / 2)
88+
{
89+
CurrentFPS = fpsCounter.Average();
90+
fpsCounter.Clear();
91+
}
92+
}
93+
94+
_fpsStopWatch.Restart();
95+
96+
// Get frame
97+
videoCapture.Read(frame);
98+
99+
if (!frame.Empty())
100+
{
101+
// Optional flip
102+
Mat workFrame = FlipImageY ? frame.Flip(FlipMode.Y) : frame;
103+
workFrame = FlipImageX ? workFrame.Flip(FlipMode.X) : workFrame;
104+
105+
if (!cancellationToken.IsCancellationRequested)
106+
{
107+
ImageGrabbed.OnNext(new ImageGrabbedData(workFrame, CurrentFPS));
108+
}
109+
}
110+
}
111+
else
112+
{
113+
// Display frame rate speed to get desired display frame rate. We use half the expected time to consider time spent executing other code
114+
var fpsDelay = (fpsMilliseconds / 2) - (int)_fpsStopWatch.ElapsedMilliseconds;
115+
if (fpsDelay > 0) await Task.Delay(fpsDelay, CancellationToken.None);
116+
}
117+
}
118+
119+
videoCapture.Release();
120+
// TODO: Refactor this to Thread or in Caller
121+
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
122+
{
123+
IsGrabbing = false;
124+
CurrentFPS = 0;
125+
});
126+
GrabContinuousStopped?.Invoke();
127+
}
128+
129+
public async Task GrabContinuous_Stop()
130+
{
131+
// Check
132+
if (_cts is null) return;
133+
134+
// If "Dispose" gets called before Stop
135+
if (_cts.IsCancellationRequested) return;
136+
137+
_cts.Cancel();
138+
139+
// Wait for grab to finish - TODO: Refactor this!
140+
await Task.Delay(250);
141+
}
142+
143+
public void Dispose()
144+
{
145+
_cts?.Cancel();
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)