├── .gitignore
├── README.md
├── assets
├── model
│ └── 2d57957ce10f4489a43725da328a5ba9.ONNX.zip
├── test
│ ├── maksssksksss348.png
│ ├── maksssksksss432.png
│ ├── maksssksksss468.png
│ ├── maksssksksss490.png
│ ├── maksssksksss504.png
│ ├── maksssksksss528.png
│ ├── maksssksksss530.png
│ ├── maksssksksss531.png
│ ├── maksssksksss532.png
│ ├── maksssksksss537.png
│ ├── maksssksksss561.png
│ ├── maksssksksss565.png
│ ├── maksssksksss571.png
│ └── maksssksksss598.png
└── train
│ ├── maksssksksss0.png
│ ├── maksssksksss10.png
│ ├── maksssksksss126.png
│ ├── maksssksksss128.png
│ ├── maksssksksss162.png
│ ├── maksssksksss180.png
│ ├── maksssksksss188.png
│ ├── maksssksksss210.png
│ ├── maksssksksss295.png
│ ├── maksssksksss35.png
│ ├── maksssksksss37.png
│ ├── maksssksksss39.png
│ ├── maksssksksss51.png
│ ├── maksssksksss6.png
│ ├── maksssksksss60.png
│ ├── maksssksksss62.png
│ ├── maksssksksss72.png
│ ├── maksssksksss8.png
│ ├── maksssksksss88.png
│ └── maksssksksss96.png
├── customvision-train
├── README.md
├── customvision-1.jpg
├── customvision-2.jpg
├── customvision-3.jpg
├── customvision-4.jpg
├── customvision-5.jpg
├── customvision-6.jpg
├── customvision-export-1.jpg
├── customvision-export-2.jpg
├── customvision-test-1.jpg
├── customvision-test-2.jpg
├── customvision-test-3.jpg
├── customvision-test-4.jpg
├── customvision-test-5.jpg
├── customvision-train-1.jpg
└── customvision-train-2.jpg
├── images
├── assets-test.jpg
├── assets-train.jpg
├── labelstudio-image.jpg
├── makesense-image.jpg
├── showcase.gif
└── vott-image.jpg
└── src
├── MachineLearning.ObjectDetect.WPF.sln
├── MachineLearning.ObjectDetect.WPF
├── App.xaml
├── App.xaml.cs
├── ColorExtensions.cs
├── ControlStyles
│ └── ToggleSwitch.xaml
├── FodyWeavers.xml
├── FodyWeavers.xsd
├── MachineLearning.ObjectDetect.WPF.csproj
├── OnnxModels
│ ├── TinyYolo2_model.onnx
│ └── facemask.ONNX.zip
├── ReactiveMetroWindow.cs
├── Services
│ └── CameraOpenCv.cs
├── TestImages
│ ├── 00_maksssksksss598.png
│ ├── 01_maksssksksss348.png
│ ├── 02_maksssksksss531.png
│ ├── maksssksksss432.png
│ ├── maksssksksss490.png
│ ├── maksssksksss504.png
│ ├── maksssksksss528.png
│ ├── maksssksksss530.png
│ ├── maksssksksss532.png
│ ├── maksssksksss537.png
│ ├── maksssksksss561.png
│ ├── maksssksksss565.png
│ └── maksssksksss571.png
├── ViewModels
│ ├── FolderViewModel.cs
│ ├── MainWindowViewModel.cs
│ ├── SelectViewModel.cs
│ └── WebcamViewModel.cs
└── Views
│ ├── FolderView.xaml
│ ├── FolderView.xaml.cs
│ ├── MainWindow.xaml
│ ├── MainWindow.xaml.cs
│ ├── SelectView.xaml
│ ├── SelectView.xaml.cs
│ ├── WebcamView.xaml
│ └── WebcamView.xaml.cs
└── OnnxObjectDetection
├── BoundingBox.cs
├── BoundingBoxDimensions.cs
├── ML
├── DataModels
│ ├── CustomVisionModel.cs
│ ├── CustomVisionPrediction.cs
│ ├── IOnnxModel.cs
│ ├── ImageInputData.cs
│ ├── TinyYoloModel.cs
│ └── TinyYoloPrediction.cs
└── OnnxModelConfigurator.cs
├── OnnxObjectDetection.csproj
└── OnnxOutputParser.cs
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # MightyMoose
150 | *.mm.*
151 | AutoTest.Net/
152 |
153 | # Web workbench (sass)
154 | .sass-cache/
155 |
156 | # Installshield output folder
157 | [Ee]xpress/
158 |
159 | # DocProject is a documentation generator add-in
160 | DocProject/buildhelp/
161 | DocProject/Help/*.HxT
162 | DocProject/Help/*.HxC
163 | DocProject/Help/*.hhc
164 | DocProject/Help/*.hhk
165 | DocProject/Help/*.hhp
166 | DocProject/Help/Html2
167 | DocProject/Help/html
168 |
169 | # Click-Once directory
170 | publish/
171 |
172 | # Publish Web Output
173 | *.[Pp]ublish.xml
174 | *.azurePubxml
175 | # Note: Comment the next line if you want to checkin your web deploy settings,
176 | # but database connection strings (with potential passwords) will be unencrypted
177 | *.pubxml
178 | *.publishproj
179 |
180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
181 | # checkin your Azure Web App publish settings, but sensitive information contained
182 | # in these scripts will be unencrypted
183 | PublishScripts/
184 |
185 | # NuGet Packages
186 | *.nupkg
187 | # NuGet Symbol Packages
188 | *.snupkg
189 | # The packages folder can be ignored because of Package Restore
190 | **/[Pp]ackages/*
191 | # except build/, which is used as an MSBuild target.
192 | !**/[Pp]ackages/build/
193 | # Uncomment if necessary however generally it will be regenerated when needed
194 | #!**/[Pp]ackages/repositories.config
195 | # NuGet v3's project.json files produces more ignorable files
196 | *.nuget.props
197 | *.nuget.targets
198 |
199 | # Microsoft Azure Build Output
200 | csx/
201 | *.build.csdef
202 |
203 | # Microsoft Azure Emulator
204 | ecf/
205 | rcf/
206 |
207 | # Windows Store app package directories and files
208 | AppPackages/
209 | BundleArtifacts/
210 | Package.StoreAssociation.xml
211 | _pkginfo.txt
212 | *.appx
213 | *.appxbundle
214 | *.appxupload
215 |
216 | # Visual Studio cache files
217 | # files ending in .cache can be ignored
218 | *.[Cc]ache
219 | # but keep track of directories ending in .cache
220 | !?*.[Cc]ache/
221 |
222 | # Others
223 | ClientBin/
224 | ~$*
225 | *~
226 | *.dbmdl
227 | *.dbproj.schemaview
228 | *.jfm
229 | *.pfx
230 | *.publishsettings
231 | orleans.codegen.cs
232 |
233 | # Including strong name files can present a security risk
234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
235 | #*.snk
236 |
237 | # Since there are multiple workflows, uncomment next line to ignore bower_components
238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
239 | #bower_components/
240 |
241 | # RIA/Silverlight projects
242 | Generated_Code/
243 |
244 | # Backup & report files from converting an old project file
245 | # to a newer Visual Studio version. Backup files are not needed,
246 | # because we have git ;-)
247 | _UpgradeReport_Files/
248 | Backup*/
249 | UpgradeLog*.XML
250 | UpgradeLog*.htm
251 | ServiceFabricBackup/
252 | *.rptproj.bak
253 |
254 | # SQL Server files
255 | *.mdf
256 | *.ldf
257 | *.ndf
258 |
259 | # Business Intelligence projects
260 | *.rdl.data
261 | *.bim.layout
262 | *.bim_*.settings
263 | *.rptproj.rsuser
264 | *- [Bb]ackup.rdl
265 | *- [Bb]ackup ([0-9]).rdl
266 | *- [Bb]ackup ([0-9][0-9]).rdl
267 |
268 | # Microsoft Fakes
269 | FakesAssemblies/
270 |
271 | # GhostDoc plugin setting file
272 | *.GhostDoc.xml
273 |
274 | # Node.js Tools for Visual Studio
275 | .ntvs_analysis.dat
276 | node_modules/
277 |
278 | # Visual Studio 6 build log
279 | *.plg
280 |
281 | # Visual Studio 6 workspace options file
282 | *.opt
283 |
284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
285 | *.vbw
286 |
287 | # Visual Studio LightSwitch build output
288 | **/*.HTMLClient/GeneratedArtifacts
289 | **/*.DesktopClient/GeneratedArtifacts
290 | **/*.DesktopClient/ModelManifest.xml
291 | **/*.Server/GeneratedArtifacts
292 | **/*.Server/ModelManifest.xml
293 | _Pvt_Extensions
294 |
295 | # Paket dependency manager
296 | .paket/paket.exe
297 | paket-files/
298 |
299 | # FAKE - F# Make
300 | .fake/
301 |
302 | # CodeRush personal settings
303 | .cr/personal
304 |
305 | # Python Tools for Visual Studio (PTVS)
306 | __pycache__/
307 | *.pyc
308 |
309 | # Cake - Uncomment if you are using it
310 | # tools/**
311 | # !tools/packages.config
312 |
313 | # Tabs Studio
314 | *.tss
315 |
316 | # Telerik's JustMock configuration file
317 | *.jmconfig
318 |
319 | # BizTalk build output
320 | *.btp.cs
321 | *.btm.cs
322 | *.odx.cs
323 | *.xsd.cs
324 |
325 | # OpenCover UI analysis results
326 | OpenCover/
327 |
328 | # Azure Stream Analytics local run output
329 | ASALocalRun/
330 |
331 | # MSBuild Binary and Structured Log
332 | *.binlog
333 |
334 | # NVidia Nsight GPU debugger configuration file
335 | *.nvuser
336 |
337 | # MFractors (Xamarin productivity tool) working folder
338 | .mfractor/
339 |
340 | # Local History for Visual Studio
341 | .localhistory/
342 |
343 | # BeatPulse healthcheck temp database
344 | healthchecksdb
345 |
346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
347 | MigrationBackup/
348 |
349 | # Ionide (cross platform F# VS Code tools) working folder
350 | .ionide/
351 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Machine Learning Object Detection in .NET with WPF App
2 | Complete sample for object detection in .NET using Machine Learning and WPF. From image labeling to training to final app.
3 |
4 | > For sample/demonstration/understanding of the full cycle I will label and train with just a few images of a very simple problem. Labeling and model training depending the domain and complexity of the project is something best done by a Data Scientist specialist.
5 | For .NET developers I think the important part is Part 3 so they can integrate with their software and add value. Labeling and model training can be outsource to a specialist or use one of the several public models available made by experts.
6 |
7 | 
8 |
9 | ## Part 1 - Labeling
10 | The first step to detect objects in images/video is to label them. There are several applications and websites for this. Opensource, comercial, local only, cloud backend to fit everyone needs/taste. Here is a short list of some of them:
11 |
12 |
13 | * [VoTT - Microsoft Visual Object Tagging Tool](https://github.com/microsoft/VoTT)
14 |
15 | Can be run as a desktop application and works with local files or directly [online](https://vott.z22.web.core.windows.net/) using Azure Blob Storage. It allows to export to several formats (Pascal VOC, TensorFlow recods, Azure Custom Vision Service, etc)
16 |
17 | 
18 |
19 |
20 | * [Azure Custom Vision Portal](https://www.customvision.ai/)
21 |
22 | It's part of Azure Cognitive Services and allows to upload images and label them directly in the web.
23 |
24 | 
25 |
26 |
27 | * [Label Studio](https://labelstud.io/)
28 |
29 | Open Source data labeling tool. Apart from labeling images for object detection it can also work with audio, texts, etc. You can run it locally with Docker in one minute.
30 |
31 | 
32 |
33 |
34 | * [Make Sense](https://www.makesense.ai/)
35 |
36 | Open Source annotation tool. It runs directly on the web and allows to export the bounding box labels in many formats.
37 |
38 | 
39 |
40 | * Comercial options (some with Free Tier)
41 | * [Super Annotate](https://superannotate.com/)
42 | * [Clarifai](https://www.clarifai.com/)
43 | * [Supervisely](https://supervise.ly/)
44 | * [Labelbox](https://labelbox.com/)
45 |
46 |
47 | ## Part 2 - Model training
48 |
49 | * [Azure Custom Vision Portal](https://www.customvision.ai/)
50 |
51 | For this sample I used this simple tool for labeling and training. I used the [facemask dataset](https://www.kaggle.com/sshikamaru/face-mask-detection) from Kaggle and I just used 20 images for training and a few for testing. Labeling took me about two minutes and I did it very rough and model training took just 5 minutes.
52 |
53 | [See more details/pictures steps here](customvision-train)
54 |
55 | 
56 |
57 | * [TensorFlow](https://www.tensorflow.org/)
58 |
59 | ML.NET can work with TensorFlow models or you can convert to [ONNX format](https://onnx.ai/)
60 |
61 | * [PyTorch](https://pytorch.org/)
62 |
63 | If you are comfortable with Python and export to ONNX
64 |
65 | ## Part 3 - WPF Application
66 | In this sample I chose to use a .NET 5 WPF application so that the sample is helpful for .NET desktop developers. Obviously deploying the previous model as a Docker container or a REST API endpoint for web use is also possible and maybe even simpler and they are several samples/tutorials online for this.
67 |
68 | The sample is based on [Microsoft Machine Learning Sample](https://github.com/dotnet/machinelearning-samples/tree/master/samples/csharp/end-to-end-apps/ObjectDetection-Onnx).
69 |
70 | You can check the source code [here](src). As with most of my WPF projects I used MahApps and ReactiveUI but those are not required.
71 |
--------------------------------------------------------------------------------
/assets/model/2d57957ce10f4489a43725da328a5ba9.ONNX.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/model/2d57957ce10f4489a43725da328a5ba9.ONNX.zip
--------------------------------------------------------------------------------
/assets/test/maksssksksss348.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss348.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss432.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss432.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss468.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss468.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss490.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss490.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss504.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss504.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss528.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss528.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss530.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss530.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss531.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss531.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss532.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss532.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss537.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss537.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss561.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss561.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss565.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss565.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss571.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss571.png
--------------------------------------------------------------------------------
/assets/test/maksssksksss598.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/test/maksssksksss598.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss0.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss10.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss126.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss126.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss128.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss162.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss162.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss180.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss188.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss188.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss210.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss210.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss295.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss295.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss35.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss35.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss37.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss37.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss39.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss39.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss51.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss51.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss6.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss60.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss62.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss62.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss72.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss8.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss88.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss88.png
--------------------------------------------------------------------------------
/assets/train/maksssksksss96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/assets/train/maksssksksss96.png
--------------------------------------------------------------------------------
/customvision-train/README.md:
--------------------------------------------------------------------------------
1 | # Microsoft Custom Vision training and test
2 | Some screenshots I made while training to show how simple it is. This is not meant to be a step by step guide, you can find several tutorials online.
3 |
4 | ## Create project
5 |
6 | 
7 |
8 |
9 |
10 | 
11 |
12 |
13 |
14 | ## Label
15 |
16 | 
17 |
18 |
19 |
20 | 
21 |
22 |
23 |
24 | 
25 |
26 |
27 |
28 | 
29 |
30 |
31 |
32 | ## Train
33 |
34 | 
35 |
36 |
37 |
38 | 
39 |
40 |
41 |
42 | ## Test
43 |
44 | 
45 |
46 |
47 |
48 | 
49 |
50 |
51 |
52 | 
53 |
54 |
55 |
56 | 
57 |
58 |
59 |
60 | 
61 |
62 |
63 |
64 | ## Export
65 |
66 | 
67 |
68 |
69 |
70 | 
71 |
72 |
73 |
--------------------------------------------------------------------------------
/customvision-train/customvision-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-1.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-2.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-3.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-4.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-5.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-6.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-export-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-export-1.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-export-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-export-2.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-test-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-test-1.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-test-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-test-2.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-test-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-test-3.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-test-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-test-4.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-test-5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-test-5.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-train-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-train-1.jpg
--------------------------------------------------------------------------------
/customvision-train/customvision-train-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/customvision-train/customvision-train-2.jpg
--------------------------------------------------------------------------------
/images/assets-test.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/images/assets-test.jpg
--------------------------------------------------------------------------------
/images/assets-train.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/images/assets-train.jpg
--------------------------------------------------------------------------------
/images/labelstudio-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/images/labelstudio-image.jpg
--------------------------------------------------------------------------------
/images/makesense-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/images/makesense-image.jpg
--------------------------------------------------------------------------------
/images/showcase.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/images/showcase.gif
--------------------------------------------------------------------------------
/images/vott-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/images/vott-image.jpg
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.31019.35
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MachineLearning.ObjectDetect.WPF", "MachineLearning.ObjectDetect.WPF\MachineLearning.ObjectDetect.WPF.csproj", "{67D5581D-339D-4C12-AC3A-7B69FA3DB876}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnnxObjectDetection", "OnnxObjectDetection\OnnxObjectDetection.csproj", "{63A49E42-E567-434D-AC0C-BA8AC185F994}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {67D5581D-339D-4C12-AC3A-7B69FA3DB876}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {67D5581D-339D-4C12-AC3A-7B69FA3DB876}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {67D5581D-339D-4C12-AC3A-7B69FA3DB876}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {67D5581D-339D-4C12-AC3A-7B69FA3DB876}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {63A49E42-E567-434D-AC0C-BA8AC185F994}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {63A49E42-E567-434D-AC0C-BA8AC185F994}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {63A49E42-E567-434D-AC0C-BA8AC185F994}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {63A49E42-E567-434D-AC0C-BA8AC185F994}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {6BE19C12-AFE7-4F70-A694-6D68C817F698}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/App.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/App.xaml.cs:
--------------------------------------------------------------------------------
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;
10 |
11 | namespace MachineLearning.ObjectDetect.WPF
12 | {
13 | public partial class App : Application
14 | {
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();
66 | services.AddTransient();
67 |
68 | services.AddTransient();
69 | services.AddTransient();
70 | services.AddTransient();
71 |
72 | // Manual register views
73 | services.AddTransient(typeof(IViewFor), typeof(Views.SelectView));
74 | services.AddTransient(typeof(IViewFor), typeof(Views.FolderView));
75 | services.AddTransient(typeof(IViewFor), typeof(Views.WebcamView));
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/ColorExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace MachineLearning.ObjectDetect.WPF
2 | {
3 | internal static class ColorExtensions
4 | {
5 | internal static System.Windows.Media.Color ToMediaColor(this System.Drawing.Color drawingColor) => System.Windows.Media.Color.FromArgb(drawingColor.A, drawingColor.R, drawingColor.G, drawingColor.B);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/ControlStyles/ToggleSwitch.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 | 0
8 |
9 |
15 |
16 |
494 |
495 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/FodyWeavers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/FodyWeavers.xsd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Used to control if the On_PropertyName_Changed feature is enabled.
13 |
14 |
15 |
16 |
17 | Used to control if the Dependent properties feature is enabled.
18 |
19 |
20 |
21 |
22 | Used to control if the IsChanged property feature is enabled.
23 |
24 |
25 |
26 |
27 | 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.
28 |
29 |
30 |
31 |
32 | Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.
33 |
34 |
35 |
36 |
37 | Used to control if equality checks should use the Equals method resolved from the base class.
38 |
39 |
40 |
41 |
42 | Used to control if equality checks should use the static Equals method resolved from the base class.
43 |
44 |
45 |
46 |
47 | Used to turn off build warnings from this weaver.
48 |
49 |
50 |
51 |
52 | Used to turn off build warnings about mismatched On_PropertyName_Changed methods.
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
61 |
62 |
63 |
64 |
65 | A comma-separated list of error codes that can be safely ignored in assembly verification.
66 |
67 |
68 |
69 |
70 | 'false' to turn off automatic generation of the XML Schema file.
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/MachineLearning.ObjectDetect.WPF.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net5.0-windows
6 | true
7 | preview
8 | enable
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | all
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | PreserveNewest
38 |
39 |
40 | PreserveNewest
41 |
42 |
43 | PreserveNewest
44 |
45 |
46 | PreserveNewest
47 |
48 |
49 | PreserveNewest
50 |
51 |
52 | PreserveNewest
53 |
54 |
55 | PreserveNewest
56 |
57 |
58 | PreserveNewest
59 |
60 |
61 | PreserveNewest
62 |
63 |
64 | PreserveNewest
65 |
66 |
67 | PreserveNewest
68 |
69 |
70 | PreserveNewest
71 |
72 |
73 | PreserveNewest
74 |
75 |
76 | PreserveNewest
77 |
78 |
79 | PreserveNewest
80 |
81 |
82 | PreserveNewest
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/OnnxModels/TinyYolo2_model.onnx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/OnnxModels/TinyYolo2_model.onnx
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/OnnxModels/facemask.ONNX.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/OnnxModels/facemask.ONNX.zip
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/ReactiveMetroWindow.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Windows;
5 |
6 | using MahApps.Metro.Controls;
7 | using ReactiveUI;
8 |
9 | namespace MachineLearning.ObjectDetect.WPF
10 | {
11 | public class ReactiveMetroWindow : MetroWindow, IViewFor where TViewModel : class
12 | {
13 | public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel), typeof(TViewModel), typeof(ReactiveMetroWindow), new PropertyMetadata(null));
14 |
15 | ///
16 | /// Gets the binding root view model.
17 | ///
18 | public TViewModel? BindingRoot => ViewModel;
19 |
20 | public TViewModel? ViewModel
21 | {
22 | get => (TViewModel)GetValue(ViewModelProperty);
23 | set => SetValue(ViewModelProperty, value);
24 | }
25 |
26 | object? IViewFor.ViewModel
27 | {
28 | get => ViewModel;
29 | set => ViewModel = (TViewModel?)value;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/Services/CameraOpenCv.cs:
--------------------------------------------------------------------------------
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 | ///
29 | /// Enumerates the connected cameras.
30 | ///
31 | public static IEnumerable 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 ImageGrabbed { get; } = new Subject();
38 | public event CameraOpenCvEvents.GrabContinuousStartedEventHandler? GrabContinuousStarted;
39 | public event CameraOpenCvEvents.GrabContinuousStoppedEventHandler? GrabContinuousStopped;
40 | public bool IsGrabbing { get; private set; }
41 |
42 | ///
43 | /// Frame rate used to display video.
44 | ///
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();
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 | }
149 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/00_maksssksksss598.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/00_maksssksksss598.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/01_maksssksksss348.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/01_maksssksksss348.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/02_maksssksksss531.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/02_maksssksksss531.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss432.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss432.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss490.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss490.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss504.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss504.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss528.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss528.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss530.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss530.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss532.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss532.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss537.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss537.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss561.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss561.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss565.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss565.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss571.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gerardo-lijs/MachineLearning-ObjectDetect-WPF/2e48f55b95031f7abcf012ce3dee20fb2cf9918f/src/MachineLearning.ObjectDetect.WPF/TestImages/maksssksksss571.png
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/ViewModels/FolderViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reactive;
5 | using System.Reactive.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | using System.Drawing;
10 | using System.Windows.Media.Imaging;
11 |
12 | using ReactiveUI;
13 | using ReactiveUI.Fody.Helpers;
14 | using Splat;
15 |
16 | using OnnxObjectDetection;
17 |
18 | namespace MachineLearning.ObjectDetect.WPF.ViewModels
19 | {
20 | public class FolderViewModel : ReactiveObject, IRoutableViewModel
21 | {
22 | public string UrlPathSegment => "FolderView";
23 | public IScreen HostScreen { get; }
24 | private readonly MainWindowViewModel _mainViewModel;
25 |
26 | // Commands
27 | public ReactiveCommand NavigateBack { get; }
28 | public ReactiveCommand PrevImage { get; }
29 | public ReactiveCommand NextImage { get; }
30 | public ReactiveCommand SelectImageFolder { get; }
31 |
32 | [Reactive] public string ImageFolderPath { get; private set; } = string.Empty;
33 | [Reactive] public List ImageList { get; private set; } = new List();
34 | [Reactive] public int ImageCurrentIndex { get; private set; }
35 |
36 | [Reactive] public long DetectMilliseconds { get; private set; }
37 | [Reactive] public BitmapSource? DetectImageSource { get; private set; }
38 | public List FilteredBoundingBoxes { get; private set; } = new List();
39 |
40 | // Interactions
41 | public readonly Interaction DrawOverlays = new Interaction();
42 |
43 | public FolderViewModel(IScreen? screen = null)
44 | {
45 | HostScreen = screen ?? Locator.Current.GetService();
46 | _mainViewModel = HostScreen as MainWindowViewModel ?? throw new Exception("IScreen must be of type MainWindowViewModel");
47 |
48 | // Create command
49 | NavigateBack = ReactiveCommand.CreateFromTask(NavigateBackImpl);
50 | PrevImage = ReactiveCommand.CreateFromTask(PrevImageImpl);
51 | NextImage = ReactiveCommand.CreateFromTask(NextImageImpl);
52 | SelectImageFolder = ReactiveCommand.CreateFromTask(SelectImageFolderImpl);
53 |
54 | // Observables
55 | this.WhenAnyValue(x => x.ImageFolderPath)
56 | .Skip(1)
57 | .Subscribe(folder =>
58 | {
59 | if (string.IsNullOrWhiteSpace(folder)) return;
60 | 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();
61 | });
62 |
63 | // Load image list
64 | ImageFolderPath = System.IO.Path.Combine(Environment.CurrentDirectory, "TestImages");
65 | }
66 |
67 | private async Task NavigateBackImpl()
68 | {
69 | await _mainViewModel.Router.NavigateBack.Execute();
70 | }
71 |
72 | private async Task SelectImageFolderImpl()
73 | {
74 | using var dialog = new System.Windows.Forms.FolderBrowserDialog
75 | {
76 | ShowNewFolderButton = false,
77 | SelectedPath = ImageFolderPath
78 | };
79 |
80 | if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
81 | {
82 | ImageFolderPath = dialog.SelectedPath;
83 | ImageCurrentIndex = 0;
84 | if (ImageList.Count > 0)
85 | {
86 | await NextImage.Execute();
87 | }
88 | }
89 | }
90 |
91 | private async Task PrevImageImpl()
92 | {
93 | if (ImageCurrentIndex <= 1) return;
94 | ImageCurrentIndex--;
95 | await LoadAndDetectImage(ImageList[ImageCurrentIndex - 1]);
96 | }
97 |
98 | private async Task NextImageImpl()
99 | {
100 | if (ImageList.Count == ImageCurrentIndex) return;
101 | ImageCurrentIndex++;
102 | await LoadAndDetectImage(ImageList[ImageCurrentIndex - 1]);
103 | }
104 |
105 | private async Task DetectImage(Bitmap bitmap)
106 | {
107 | var imageInputData = new ImageInputData { Image = bitmap };
108 |
109 | var sw = System.Diagnostics.Stopwatch.StartNew();
110 |
111 | FilteredBoundingBoxes = _mainViewModel.DetectObjects(imageInputData);
112 |
113 | // Time spent for detection by ML.NET
114 | DetectMilliseconds = sw.ElapsedMilliseconds;
115 |
116 | await DrawOverlays.Handle(Unit.Default);
117 | }
118 |
119 | private async Task LoadAndDetectImage(string filename)
120 | {
121 | var bitmapImage = new Bitmap(filename);
122 | DetectImageSource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(bitmapImage.GetHbitmap(), IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
123 | await DetectImage(bitmapImage);
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/ViewModels/MainWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reactive;
5 | using System.Reactive.Linq;
6 | using System.Reactive.Disposables;
7 | using System.Threading.Tasks;
8 |
9 | using Splat;
10 | using ReactiveUI;
11 | using ReactiveUI.Fody.Helpers;
12 | using ControlzEx.Theming;
13 |
14 | using Microsoft.ML;
15 | using OnnxObjectDetection;
16 |
17 | namespace MachineLearning.ObjectDetect.WPF.ViewModels
18 | {
19 | public class MainWindowViewModel : ReactiveObject, IScreen
20 | {
21 | public RoutingState Router { get; }
22 |
23 | public string ModelsDirectory { get; }
24 |
25 | private OnnxOutputParser? OutputParser;
26 | private PredictionEngine? CustomVisionPredictionEngine;
27 | private PredictionEngine? TinyYoloPredictionEngine;
28 |
29 | [Reactive] public bool IsLightTheme { get; set; }
30 |
31 | public MainWindowViewModel()
32 | {
33 | Locator.CurrentMutable.RegisterConstant(this, typeof(IScreen));
34 |
35 | ModelsDirectory = System.IO.Path.Combine(Environment.CurrentDirectory, @"OnnxModels");
36 |
37 | // Initialize the Router.
38 | Router = new RoutingState();
39 |
40 | // Current theme
41 | var theme = ThemeManager.Current.DetectTheme();
42 | IsLightTheme = theme is null || theme.BaseColorScheme == ThemeManager.BaseColorLight;
43 |
44 | // Theme change
45 | this.WhenAnyValue(x => x.IsLightTheme)
46 | .Skip(1)
47 | .Subscribe(isLightTheme =>
48 | {
49 | if (isLightTheme)
50 | ThemeManager.Current.ChangeThemeBaseColor(System.Windows.Application.Current, "Light");
51 | else
52 | ThemeManager.Current.ChangeThemeBaseColor(System.Windows.Application.Current, "Dark");
53 | });
54 | }
55 |
56 | public void LoadModel(string modelFilename)
57 | {
58 | var modelFullFilename = System.IO.Path.Combine(ModelsDirectory, modelFilename);
59 |
60 | // Load Onnx model
61 | if (modelFilename.EndsWith(".zip"))
62 | {
63 | var customVisionModel = new CustomVisionModel(modelFullFilename);
64 | var modelConfigurator = new OnnxModelConfigurator(customVisionModel);
65 |
66 | OutputParser = new OnnxOutputParser(customVisionModel);
67 | CustomVisionPredictionEngine = modelConfigurator.GetMlNetPredictionEngine();
68 | TinyYoloPredictionEngine = null;
69 | }
70 | else
71 | {
72 | var tinyYoloModel = new TinyYoloModel(modelFullFilename);
73 | var modelConfigurator = new OnnxModelConfigurator(tinyYoloModel);
74 |
75 | OutputParser = new OnnxOutputParser(tinyYoloModel);
76 | TinyYoloPredictionEngine = modelConfigurator.GetMlNetPredictionEngine();
77 | CustomVisionPredictionEngine = null;
78 | }
79 | }
80 |
81 | public List DetectObjects(ImageInputData imageInputData)
82 | {
83 | if (OutputParser is null) throw new Exception("Model not loaded.");
84 |
85 | var labels = CustomVisionPredictionEngine?.Predict(imageInputData).PredictedLabels ?? TinyYoloPredictionEngine?.Predict(imageInputData).PredictedLabels;
86 | var boundingBoxes = OutputParser.ParseOutputs(labels);
87 | var filteredBoxes = OutputParser.FilterBoundingBoxes(boundingBoxes, 5, 0.5f);
88 | return filteredBoxes;
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/ViewModels/SelectViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reactive;
5 | using System.Reactive.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | using System.Drawing;
10 | using System.Windows.Media.Imaging;
11 |
12 | using ReactiveUI;
13 | using ReactiveUI.Fody.Helpers;
14 | using Splat;
15 |
16 | using OnnxObjectDetection;
17 |
18 | namespace MachineLearning.ObjectDetect.WPF.ViewModels
19 | {
20 | public class SelectViewModel : ReactiveObject, IRoutableViewModel
21 | {
22 | public string UrlPathSegment => "SelectView";
23 | public IScreen HostScreen { get; }
24 | private readonly MainWindowViewModel _mainViewModel;
25 |
26 | public List Models { get; }
27 | [Reactive] public string? ModelSelected { get; set; }
28 |
29 | // Commands
30 | public ReactiveCommand FolderViewSelect { get; }
31 | public ReactiveCommand WebcamViewSelect { get; }
32 |
33 | public SelectViewModel(IScreen? screen = null)
34 | {
35 | HostScreen = screen ?? Locator.Current.GetService();
36 | _mainViewModel = HostScreen as MainWindowViewModel ?? throw new Exception("IScreen must be of type MainWindowViewModel");
37 |
38 | // Enumerate models
39 | Models = System.IO.Directory.GetFiles(_mainViewModel.ModelsDirectory).Where(x => x.EndsWith(".zip") || x.EndsWith(".onnx")).Select(x => System.IO.Path.GetFileName(x)).ToList();
40 | ModelSelected = Models.FirstOrDefault();
41 |
42 | // Observables
43 | this.WhenAnyValue(x => x.ModelSelected)
44 | .WhereNotNull()
45 | .Subscribe(modelFilename =>
46 | {
47 | _mainViewModel.LoadModel(modelFilename);
48 | });
49 |
50 | // Create command
51 | FolderViewSelect = ReactiveCommand.CreateFromObservable(FolderViewSelectImpl);
52 | WebcamViewSelect = ReactiveCommand.CreateFromObservable(WebcamViewSelectImpl);
53 | }
54 |
55 | private IObservable FolderViewSelectImpl()
56 | {
57 | _mainViewModel.Router.Navigate.Execute(Locator.Current.GetService()).Subscribe();
58 | return Observable.Return(Unit.Default);
59 | }
60 |
61 | private IObservable WebcamViewSelectImpl()
62 | {
63 | _mainViewModel.Router.Navigate.Execute(Locator.Current.GetService()).Subscribe();
64 | return Observable.Return(Unit.Default);
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/ViewModels/WebcamViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reactive;
5 | using System.Reactive.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using System.Reactive.Disposables;
9 |
10 | using System.Drawing;
11 |
12 | using ReactiveUI;
13 | using ReactiveUI.Fody.Helpers;
14 | using Splat;
15 |
16 | using OpenCvSharp.Extensions;
17 | using OnnxObjectDetection;
18 |
19 | namespace MachineLearning.ObjectDetect.WPF.ViewModels
20 | {
21 | public class WebcamViewModel : ReactiveObject, IActivatableViewModel, IRoutableViewModel
22 | {
23 | public string UrlPathSegment => "WebcamView";
24 | public IScreen HostScreen { get; }
25 | public ViewModelActivator Activator { get; } = new ViewModelActivator();
26 | private readonly CompositeDisposable _cleanUp = new CompositeDisposable();
27 | private readonly MainWindowViewModel _mainViewModel;
28 |
29 | public Services.CameraOpenCv CameraOpenCv { get; }
30 |
31 | public List CameraDevices { get; }
32 | [Reactive] public Services.CameraOpenCv.CameraDevice? CameraDeviceSelected { get; set; }
33 |
34 | public List FilteredBoundingBoxes { get; private set; } = new List();
35 |
36 | // Commands
37 | public ReactiveCommand NavigateBack { get; }
38 | public ReactiveCommand GrabContinuous_Start { get; }
39 | public ReactiveCommand GrabContinuous_Stop { get; }
40 |
41 | // Interactions
42 | public readonly Interaction DrawOverlays = new Interaction();
43 |
44 | [Reactive] public bool DetectObjects { get; set; }
45 |
46 | public WebcamViewModel(IScreen? screen = null)
47 | {
48 | HostScreen = screen ?? Locator.Current.GetService();
49 | _mainViewModel = HostScreen as MainWindowViewModel ?? throw new Exception("IScreen must be of type MainWindowViewModel");
50 |
51 | // Create command
52 | NavigateBack = ReactiveCommand.CreateFromTask(NavigateBackImpl);
53 | GrabContinuous_Start = ReactiveCommand.CreateFromTask(GrabContinuous_StartImpl);
54 | GrabContinuous_Stop = ReactiveCommand.CreateFromTask(GrabContinuous_StopImpl);
55 |
56 | // Enumerate cameras
57 | CameraDevices = Services.CameraOpenCv.EnumerateAllConnectedCameras().ToList();
58 | CameraDeviceSelected = CameraDevices.FirstOrDefault();
59 |
60 | // For this sample we init the camera here. We could also have a Singleton camera and get it directly with DI
61 | CameraOpenCv = new Services.CameraOpenCv();
62 | //CameraOpenCv.GrabContinuousStarted += CameraOpenCv_GrabContinuousStarted;
63 | //CameraOpenCv.GrabContinuousStopped += CameraOpenCv_GrabContinuousStopped;
64 | CameraOpenCv.ImageGrabbed.Subscribe(async imageGrabbedData =>
65 | {
66 | // Check
67 | if (!DetectObjects) return;
68 |
69 | // Detect
70 | var imageInputData = new ImageInputData { Image = imageGrabbedData.image.ToBitmap() };
71 | FilteredBoundingBoxes = _mainViewModel.DetectObjects(imageInputData);
72 | await DrawOverlays.Handle(Unit.Default);
73 | })
74 | .DisposeWith(_cleanUp);
75 |
76 | this.WhenActivated(disposables =>
77 | {
78 | Disposable
79 | .Create(() => HandleDeactivation())
80 | .DisposeWith(disposables);
81 | });
82 | }
83 |
84 | private void HandleDeactivation()
85 | {
86 | _cleanUp.Dispose();
87 | CameraOpenCv?.Dispose();
88 | }
89 |
90 | private async Task NavigateBackImpl()
91 | {
92 | await GrabContinuous_Stop.Execute();
93 | await _mainViewModel.Router.NavigateBack.Execute();
94 | }
95 |
96 | private async Task GrabContinuous_StartImpl()
97 | {
98 | // Check
99 | if (CameraDeviceSelected is null) throw new Exception("Camera device not selected.");
100 |
101 | // Grab
102 | await CameraOpenCv.GrabContinuous_Start(CameraDeviceSelected.CameraIndex);
103 | }
104 |
105 | private async Task GrabContinuous_StopImpl()
106 | {
107 | // Cancel and dispose
108 | await CameraOpenCv.GrabContinuous_Stop();
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/Views/FolderView.xaml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/Views/FolderView.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reactive;
5 | using System.Reactive.Disposables;
6 | using System.Reactive.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using System.Windows;
10 | using System.Windows.Controls;
11 | using System.Windows.Data;
12 | using System.Windows.Documents;
13 | using System.Windows.Input;
14 | using System.Windows.Media;
15 | using System.Windows.Media.Imaging;
16 | using System.Windows.Navigation;
17 | using System.Windows.Shapes;
18 |
19 | using ReactiveUI;
20 |
21 | using MachineLearning.ObjectDetect.WPF.ViewModels;
22 |
23 | namespace MachineLearning.ObjectDetect.WPF.Views
24 | {
25 | public class FolderViewBase : ReactiveUserControl { }
26 | public partial class FolderView : FolderViewBase
27 | {
28 | public FolderView()
29 | {
30 | InitializeComponent();
31 | this.WhenActivated(async disposableRegistration =>
32 | {
33 | this.WhenAnyValue(viewModel => viewModel.ViewModel).BindTo(this, view => view.DataContext).DisposeWith(disposableRegistration);
34 |
35 | // Commands
36 | this.BindCommand(ViewModel, viewModel => viewModel.NavigateBack, view => view.NavigateBackButton).DisposeWith(disposableRegistration);
37 | this.BindCommand(ViewModel, viewModel => viewModel.PrevImage, view => view.PrevImageButton).DisposeWith(disposableRegistration);
38 | this.BindCommand(ViewModel, viewModel => viewModel.NextImage, view => view.NextImageButton).DisposeWith(disposableRegistration);
39 | this.BindCommand(ViewModel, viewModel => viewModel.SelectImageFolder, view => view.SelectImageFolderButton).DisposeWith(disposableRegistration);
40 |
41 | // Image
42 | this.OneWayBind(ViewModel, viewModel => viewModel.ImageFolderPath, view => view.ImageFolderPathTextBox.Text).DisposeWith(disposableRegistration);
43 | this.OneWayBind(ViewModel, viewModel => viewModel.DetectImageSource, view => view.DetectImage.Source).DisposeWith(disposableRegistration);
44 | this.OneWayBind(ViewModel, viewModel => viewModel.DetectMilliseconds, view => view.StatusTextBlock.Text, x => $"Object detection took {x} milliseconds").DisposeWith(disposableRegistration);
45 |
46 | // Visibility
47 | this.OneWayBind(ViewModel, viewModel => viewModel.DetectMilliseconds, view => view.StatusTextBlock.Visibility, x => x > 0 ? Visibility.Visible : Visibility.Hidden).DisposeWith(disposableRegistration);
48 | this.OneWayBind(ViewModel, viewModel => viewModel.ImageCurrentIndex, view => view.PrevImageButton.Visibility, x => x <= 1 ? Visibility.Hidden : Visibility.Visible).DisposeWith(disposableRegistration);
49 | this.OneWayBind(ViewModel, viewModel => viewModel.ImageCurrentIndex, view => view.NextImageButton.Visibility, x => x == ViewModel.ImageList.Count ? Visibility.Hidden : Visibility.Visible).DisposeWith(disposableRegistration);
50 |
51 | // Interactions
52 | ViewModel.DrawOverlays.RegisterHandler(interaction =>
53 | {
54 | DrawOverlays();
55 | interaction.SetOutput(Unit.Default);
56 | }).DisposeWith(disposableRegistration);
57 |
58 | // Move to first image
59 | if (ViewModel.ImageList.Count > 0)
60 | {
61 | await ViewModel.NextImage.Execute();
62 | }
63 | });
64 | }
65 |
66 | private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e) => DrawOverlays();
67 | private void DetectImage_SizeChanged(object sender, SizeChangedEventArgs e) => DrawOverlays();
68 |
69 | private void DrawOverlays()
70 | {
71 | if (ViewModel is null) return;
72 |
73 | var originalHeight = DetectImage.ActualHeight;
74 | var originalWidth = DetectImage.ActualWidth;
75 |
76 | WebCamCanvas.Children.Clear();
77 |
78 | foreach (var box in ViewModel.FilteredBoundingBoxes)
79 | {
80 | // Process output boxes
81 | double x = Math.Max(box.Dimensions.X, 0);
82 | double y = Math.Max(box.Dimensions.Y, 0);
83 | double width = Math.Min(originalWidth - x, box.Dimensions.Width);
84 | double height = Math.Min(originalHeight - y, box.Dimensions.Height);
85 |
86 | // Fit to current image size
87 | x = originalWidth * x / OnnxObjectDetection.ImageSettings.imageWidth;
88 | y = originalHeight * y / OnnxObjectDetection.ImageSettings.imageHeight;
89 | width = originalWidth * width / OnnxObjectDetection.ImageSettings.imageWidth;
90 | height = originalHeight * height / OnnxObjectDetection.ImageSettings.imageHeight;
91 |
92 | var boxColor = box.BoxColor.ToMediaColor();
93 |
94 | var objBox = new Rectangle
95 | {
96 | Width = width,
97 | Height = height,
98 | Fill = new SolidColorBrush(Colors.Transparent),
99 | Stroke = new SolidColorBrush(boxColor),
100 | StrokeThickness = 2.0,
101 | Margin = new Thickness(x, y, 0, 0)
102 | };
103 |
104 | var objDescription = new TextBlock
105 | {
106 | Margin = new Thickness(x + 4, y + 4, 0, 0),
107 | Text = box.Description,
108 | FontWeight = FontWeights.Bold,
109 | Width = 126,
110 | Height = 21,
111 | TextAlignment = TextAlignment.Center
112 | };
113 |
114 | var objDescriptionBackground = new Rectangle
115 | {
116 | Width = 134,
117 | Height = 29,
118 | Fill = new SolidColorBrush(boxColor),
119 | Margin = new Thickness(x, y, 0, 0)
120 | };
121 |
122 | WebCamCanvas.Children.Add(objDescriptionBackground);
123 | WebCamCanvas.Children.Add(objDescription);
124 | WebCamCanvas.Children.Add(objBox);
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/Views/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 | 0
19 | 0
20 | 0
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/Views/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using System.Reactive.Disposables;
8 | using System.Windows.Controls;
9 | using System.Windows.Media;
10 | using System.Reactive;
11 | using System.Reactive.Linq;
12 |
13 | using ReactiveUI;
14 | using Splat;
15 |
16 | using MachineLearning.ObjectDetect.WPF.ViewModels;
17 |
18 | namespace MachineLearning.ObjectDetect.WPF.Views
19 | {
20 | public class MainWindowBase : ReactiveMetroWindow { }
21 | public partial class MainWindow : MainWindowBase
22 | {
23 | public MainWindow()
24 | {
25 | ViewModel = new MainWindowViewModel();
26 | InitializeComponent();
27 | this.WhenActivated(async disposableRegistration =>
28 | {
29 | this.WhenAnyValue(viewModel => viewModel.ViewModel).BindTo(this, view => view.DataContext).DisposeWith(disposableRegistration);
30 |
31 | // Router
32 | this.OneWayBind(ViewModel, viewModel => viewModel.Router, view => view.RoutedViewHost.Router).DisposeWith(disposableRegistration);
33 |
34 | this.Bind(ViewModel, viewModel => viewModel.IsLightTheme, view => view.ThemeChangeToggleSwitch.IsOn).DisposeWith(disposableRegistration);
35 |
36 | // Start with New Capture content
37 | await ViewModel.Router.Navigate.Execute(Locator.Current.GetService());
38 | });
39 | }
40 |
41 | private async void MainWindowBase_Closing(object sender, System.ComponentModel.CancelEventArgs e)
42 | {
43 | var webcamViewModel = ViewModel.Router.GetCurrentViewModel() as WebcamViewModel;
44 | if (webcamViewModel is not null)
45 | {
46 | await webcamViewModel.CameraOpenCv.GrabContinuous_Stop();
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/Views/SelectView.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/Views/SelectView.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reactive;
5 | using System.Reactive.Disposables;
6 | using System.Reactive.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using System.Windows;
10 | using System.Windows.Controls;
11 | using System.Windows.Data;
12 | using System.Windows.Documents;
13 | using System.Windows.Input;
14 | using System.Windows.Media;
15 | using System.Windows.Media.Imaging;
16 | using System.Windows.Navigation;
17 | using System.Windows.Shapes;
18 |
19 | using ReactiveUI;
20 |
21 | using MachineLearning.ObjectDetect.WPF.ViewModels;
22 |
23 | namespace MachineLearning.ObjectDetect.WPF.Views
24 | {
25 | public class SelectViewBase : ReactiveUserControl { }
26 | public partial class SelectView : SelectViewBase
27 | {
28 | public SelectView()
29 | {
30 | InitializeComponent();
31 | this.WhenActivated(disposableRegistration =>
32 | {
33 | this.WhenAnyValue(viewModel => viewModel.ViewModel).BindTo(this, view => view.DataContext).DisposeWith(disposableRegistration);
34 |
35 | // Model combo
36 | ModelCombobox.DisplayMemberPath = ".";
37 | this.OneWayBind(ViewModel, viewModel => viewModel.Models, view => view.ModelCombobox.ItemsSource).DisposeWith(disposableRegistration);
38 | this.Bind(ViewModel, viewModel => viewModel.ModelSelected, view => view.ModelCombobox.SelectedValue).DisposeWith(disposableRegistration);
39 |
40 | // Commands
41 | this.BindCommand(ViewModel, viewModel => viewModel.FolderViewSelect, view => view.FolderViewButton).DisposeWith(disposableRegistration);
42 | this.BindCommand(ViewModel, viewModel => viewModel.WebcamViewSelect, view => view.WebcamViewButton).DisposeWith(disposableRegistration);
43 |
44 | });
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/Views/WebcamView.xaml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/MachineLearning.ObjectDetect.WPF/Views/WebcamView.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reactive;
5 | using System.Reactive.Disposables;
6 | using System.Reactive.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using System.Windows;
10 | using System.Windows.Controls;
11 | using System.Windows.Data;
12 | using System.Windows.Documents;
13 | using System.Windows.Input;
14 | using System.Windows.Media;
15 | using System.Windows.Media.Imaging;
16 | using System.Windows.Navigation;
17 | using System.Windows.Shapes;
18 |
19 | using ReactiveUI;
20 | using OpenCvSharp.WpfExtensions;
21 |
22 | using MachineLearning.ObjectDetect.WPF.ViewModels;
23 |
24 | namespace MachineLearning.ObjectDetect.WPF.Views
25 | {
26 | public class WebcamViewBase : ReactiveUserControl { }
27 | public partial class WebcamView : WebcamViewBase
28 | {
29 | public WebcamView()
30 | {
31 | InitializeComponent();
32 | this.WhenActivated(disposableRegistration =>
33 | {
34 | this.WhenAnyValue(viewModel => viewModel.ViewModel).BindTo(this, view => view.DataContext).DisposeWith(disposableRegistration);
35 |
36 | // Commands
37 | this.BindCommand(ViewModel, viewModel => viewModel.NavigateBack, view => view.NavigateBackButton).DisposeWith(disposableRegistration);
38 | this.BindCommand(ViewModel, viewModel => viewModel.GrabContinuous_Start, view => view.WebcamStartButton).DisposeWith(disposableRegistration);
39 | this.BindCommand(ViewModel, viewModel => viewModel.GrabContinuous_Stop, view => view.WebcamStopButton).DisposeWith(disposableRegistration);
40 |
41 | // Device Combobox
42 | CameraDeviceComboBox.DisplayMemberPath = nameof(Services.CameraOpenCv.CameraDevice.Name);
43 | this.OneWayBind(ViewModel, viewModel => viewModel.CameraDevices, view => view.CameraDeviceComboBox.ItemsSource).DisposeWith(disposableRegistration);
44 | this.Bind(ViewModel, viewModel => viewModel.CameraDeviceSelected, view => view.CameraDeviceComboBox.SelectedValue).DisposeWith(disposableRegistration);
45 |
46 | // Start/Stop buttons
47 | this.OneWayBind(ViewModel, viewModel => viewModel.CameraOpenCv.IsGrabbing, view => view.WebcamStartButton.IsEnabled, x => !x).DisposeWith(disposableRegistration);
48 | this.OneWayBind(ViewModel, viewModel => viewModel.CameraOpenCv.IsGrabbing, view => view.WebcamStopButton.IsEnabled).DisposeWith(disposableRegistration);
49 |
50 | // Checkbox
51 | this.Bind(ViewModel, viewModel => viewModel.CameraOpenCv.FlipImageY, view => view.FlipImageYToggleSwitch.IsOn).DisposeWith(disposableRegistration);
52 | this.Bind(ViewModel, viewModel => viewModel.CameraOpenCv.FlipImageX, view => view.FlipImageXToggleSwitch.IsOn).DisposeWith(disposableRegistration);
53 | this.Bind(ViewModel, viewModel => viewModel.DetectObjects, view => view.DetectObjectsToggleSwitch.IsOn).DisposeWith(disposableRegistration);
54 |
55 | ViewModel.CameraOpenCv.ImageGrabbed.Subscribe(async imageGrabbedData =>
56 | {
57 | // Update frame in UI thread
58 | try
59 | {
60 | await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
61 | {
62 | CurrentFPSTextBlock.Text = imageGrabbedData.CurrentFPS.ToString("N1");
63 | WebcamImage.Source = imageGrabbedData.image.ToBitmapSource();
64 | });
65 | }
66 | catch (TaskCanceledException)
67 | {
68 | // App shutting down
69 | }
70 | }).DisposeWith(disposableRegistration);
71 |
72 | // Interactions
73 | ViewModel.DrawOverlays.RegisterHandler(async interaction =>
74 | {
75 | // Update frame in UI thread
76 | await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
77 | {
78 | DrawOverlays();
79 | });
80 | interaction.SetOutput(Unit.Default);
81 | }).DisposeWith(disposableRegistration);
82 |
83 | // Clean up logic to execute when the view model gets deactivated.
84 | Disposable
85 | .Create(() => HandleDeactivation())
86 | .DisposeWith(disposableRegistration);
87 | });
88 | }
89 |
90 | private void HandleDeactivation()
91 | {
92 | ViewModel.CameraOpenCv.Dispose();
93 | }
94 |
95 | private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e) => DrawOverlays();
96 | private void DetectImage_SizeChanged(object sender, SizeChangedEventArgs e) => DrawOverlays();
97 |
98 | private void DrawOverlays()
99 | {
100 | if (ViewModel is null) return;
101 |
102 | var originalHeight = WebcamImage.ActualHeight;
103 | var originalWidth = WebcamImage.ActualWidth;
104 |
105 | WebCamCanvas.Children.Clear();
106 |
107 | foreach (var box in ViewModel.FilteredBoundingBoxes)
108 | {
109 | // Process output boxes
110 | double x = Math.Max(box.Dimensions.X, 0);
111 | double y = Math.Max(box.Dimensions.Y, 0);
112 | double width = Math.Min(originalWidth - x, box.Dimensions.Width);
113 | double height = Math.Min(originalHeight - y, box.Dimensions.Height);
114 |
115 | // Fit to current image size
116 | x = originalWidth * x / OnnxObjectDetection.ImageSettings.imageWidth;
117 | y = originalHeight * y / OnnxObjectDetection.ImageSettings.imageHeight;
118 | width = originalWidth * width / OnnxObjectDetection.ImageSettings.imageWidth;
119 | height = originalHeight * height / OnnxObjectDetection.ImageSettings.imageHeight;
120 |
121 | var boxColor = box.BoxColor.ToMediaColor();
122 |
123 | var objBox = new Rectangle
124 | {
125 | Width = width,
126 | Height = height,
127 | Fill = new SolidColorBrush(Colors.Transparent),
128 | Stroke = new SolidColorBrush(boxColor),
129 | StrokeThickness = 2.0,
130 | Margin = new Thickness(x, y, 0, 0)
131 | };
132 |
133 | var objDescription = new TextBlock
134 | {
135 | Margin = new Thickness(x + 4, y + 4, 0, 0),
136 | Text = box.Description,
137 | FontWeight = FontWeights.Bold,
138 | Width = 126,
139 | Height = 21,
140 | TextAlignment = TextAlignment.Center
141 | };
142 |
143 | var objDescriptionBackground = new Rectangle
144 | {
145 | Width = 134,
146 | Height = 29,
147 | Fill = new SolidColorBrush(boxColor),
148 | Margin = new Thickness(x, y, 0, 0)
149 | };
150 |
151 | WebCamCanvas.Children.Add(objDescriptionBackground);
152 | WebCamCanvas.Children.Add(objDescription);
153 | WebCamCanvas.Children.Add(objBox);
154 | }
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/OnnxObjectDetection/BoundingBox.cs:
--------------------------------------------------------------------------------
1 | using System.Drawing;
2 |
3 | namespace OnnxObjectDetection
4 | {
5 | public class BoundingBox
6 | {
7 | public BoundingBoxDimensions Dimensions { get; set; }
8 |
9 | public string Label { get; set; }
10 |
11 | public float Confidence { get; set; }
12 |
13 | public RectangleF Rect => new RectangleF(Dimensions.X, Dimensions.Y, Dimensions.Width, Dimensions.Height);
14 |
15 | public Color BoxColor { get; set; }
16 |
17 | public string Description => $"{Label} ({Confidence * 100:0}%)";
18 |
19 | private static readonly Color[] classColors = new Color[]
20 | {
21 | Color.Khaki, Color.Fuchsia, Color.Silver, Color.RoyalBlue,
22 | Color.Green, Color.DarkOrange, Color.Purple, Color.Gold,
23 | Color.Red, Color.Aquamarine, Color.Lime, Color.AliceBlue,
24 | Color.Sienna, Color.Orchid, Color.Tan, Color.LightPink,
25 | Color.Yellow, Color.HotPink, Color.OliveDrab, Color.SandyBrown,
26 | Color.DarkTurquoise
27 | };
28 |
29 | public static Color GetColor(int index) => index < classColors.Length ? classColors[index] : classColors[index % classColors.Length];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/OnnxObjectDetection/BoundingBoxDimensions.cs:
--------------------------------------------------------------------------------
1 | namespace OnnxObjectDetection
2 | {
3 | public class BoundingBoxDimensions
4 | {
5 | public float X { get; set; }
6 | public float Y { get; set; }
7 | public float Height { get; set; }
8 | public float Width { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/OnnxObjectDetection/ML/DataModels/CustomVisionModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Compression;
4 | using System.Linq;
5 |
6 | namespace OnnxObjectDetection
7 | {
8 | public class CustomVisionModel : IOnnxModel
9 | {
10 | const string modelName = "model.onnx", labelsName = "labels.txt";
11 |
12 | private readonly string labelsPath;
13 |
14 | public string ModelPath { get; private set; }
15 |
16 | public string ModelInput { get; } = "data";
17 | public string ModelOutput { get; } = "model_outputs0";
18 |
19 | public string[] Labels { get; private set; }
20 | public (float,float)[] Anchors { get; } = { (0.573f,0.677f), (1.87f,2.06f), (3.34f,5.47f), (7.88f,3.53f), (9.77f,9.17f) };
21 |
22 | public CustomVisionModel(string modelPath)
23 | {
24 | var extractPath = Path.GetFullPath(modelPath.Replace(".zip", Path.DirectorySeparatorChar.ToString()));
25 |
26 | if (!Directory.Exists(extractPath))
27 | Directory.CreateDirectory(extractPath);
28 |
29 | ModelPath = Path.GetFullPath(Path.Combine(extractPath, modelName));
30 | labelsPath = Path.GetFullPath(Path.Combine(extractPath, labelsName));
31 |
32 | if (!File.Exists(ModelPath) || !File.Exists(labelsPath))
33 | ExtractArchive(modelPath);
34 |
35 | Labels = File.ReadAllLines(labelsPath);
36 | }
37 |
38 | void ExtractArchive(string modelPath)
39 | {
40 | using (ZipArchive archive = ZipFile.OpenRead(modelPath))
41 | {
42 | var modelEntry = archive.Entries.FirstOrDefault(e => e.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase))
43 | ?? throw new FormatException("The exported .zip archive is missing the model.onnx file");
44 |
45 | modelEntry.ExtractToFile(ModelPath);
46 |
47 | var labelsEntry = archive.Entries.FirstOrDefault(e => e.Name.Equals(labelsName, StringComparison.OrdinalIgnoreCase))
48 | ?? throw new FormatException("The exported .zip archive is missing the labels.txt file");
49 |
50 | labelsEntry.ExtractToFile(labelsPath);
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/OnnxObjectDetection/ML/DataModels/CustomVisionPrediction.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.ML.Data;
2 |
3 | namespace OnnxObjectDetection
4 | {
5 | public class CustomVisionPrediction : IOnnxObjectPrediction
6 | {
7 | [ColumnName("model_outputs0")]
8 | public float[] PredictedLabels { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/OnnxObjectDetection/ML/DataModels/IOnnxModel.cs:
--------------------------------------------------------------------------------
1 | namespace OnnxObjectDetection
2 | {
3 | public interface IOnnxModel
4 | {
5 | string ModelPath { get; }
6 |
7 | // To check Model input and output parameter names, you can
8 | // use tools like Netron: https://github.com/lutzroeder/netron
9 | string ModelInput { get; }
10 | string ModelOutput { get; }
11 |
12 | string[] Labels { get; }
13 | (float, float)[] Anchors { get; }
14 | }
15 |
16 | public interface IOnnxObjectPrediction
17 | {
18 | float[] PredictedLabels { get; set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/OnnxObjectDetection/ML/DataModels/ImageInputData.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.ML.Transforms.Image;
2 | using System.Drawing;
3 |
4 | namespace OnnxObjectDetection
5 | {
6 | public struct ImageSettings
7 | {
8 | public const int imageHeight = 416;
9 | public const int imageWidth = 416;
10 | }
11 |
12 | public class ImageInputData
13 | {
14 | [ImageType(ImageSettings.imageHeight, ImageSettings.imageWidth)]
15 | public Bitmap Image { get; set; }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/OnnxObjectDetection/ML/DataModels/TinyYoloModel.cs:
--------------------------------------------------------------------------------
1 | namespace OnnxObjectDetection
2 | {
3 | public class TinyYoloModel : IOnnxModel
4 | {
5 | public string ModelPath { get; private set; }
6 |
7 | public string ModelInput { get; } = "image";
8 | public string ModelOutput { get; } = "grid";
9 |
10 | public string[] Labels { get; } =
11 | {
12 | "aeroplane", "bicycle", "bird", "boat", "bottle",
13 | "bus", "car", "cat", "chair", "cow",
14 | "diningtable", "dog", "horse", "motorbike", "person",
15 | "pottedplant", "sheep", "sofa", "train", "tvmonitor"
16 | };
17 |
18 | public (float,float)[] Anchors { get; } = { (1.08f,1.19f), (3.42f,4.41f), (6.63f,11.38f), (9.42f,5.11f), (16.62f,10.52f) };
19 |
20 | public TinyYoloModel(string modelPath)
21 | {
22 | ModelPath = modelPath;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/OnnxObjectDetection/ML/DataModels/TinyYoloPrediction.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.ML.Data;
2 |
3 | namespace OnnxObjectDetection
4 | {
5 | public class TinyYoloPrediction : IOnnxObjectPrediction
6 | {
7 | [ColumnName("grid")]
8 | public float[] PredictedLabels { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/OnnxObjectDetection/ML/OnnxModelConfigurator.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.ML;
2 | using Microsoft.ML.Transforms.Image;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 |
6 | namespace OnnxObjectDetection
7 | {
8 | public class OnnxModelConfigurator
9 | {
10 | private readonly MLContext mlContext;
11 | private readonly ITransformer mlModel;
12 |
13 | public OnnxModelConfigurator(IOnnxModel onnxModel)
14 | {
15 | mlContext = new MLContext();
16 | // Model creation and pipeline definition for images needs to run just once,
17 | // so calling it from the constructor:
18 | mlModel = SetupMlNetModel(onnxModel);
19 | }
20 |
21 | private ITransformer SetupMlNetModel(IOnnxModel onnxModel)
22 | {
23 | var dataView = mlContext.Data.LoadFromEnumerable(new List());
24 |
25 | var pipeline = mlContext.Transforms.ResizeImages(resizing: ImageResizingEstimator.ResizingKind.Fill, outputColumnName: onnxModel.ModelInput, imageWidth: ImageSettings.imageWidth, imageHeight: ImageSettings.imageHeight, inputColumnName: nameof(ImageInputData.Image))
26 | .Append(mlContext.Transforms.ExtractPixels(outputColumnName: onnxModel.ModelInput))
27 | .Append(mlContext.Transforms.ApplyOnnxModel(modelFile: onnxModel.ModelPath, outputColumnName: onnxModel.ModelOutput, inputColumnName: onnxModel.ModelInput));
28 |
29 | var mlNetModel = pipeline.Fit(dataView);
30 |
31 | return mlNetModel;
32 | }
33 |
34 | public PredictionEngine GetMlNetPredictionEngine()
35 | where T : class, IOnnxObjectPrediction, new()
36 | {
37 | return mlContext.Model.CreatePredictionEngine(mlModel);
38 | }
39 |
40 | public void SaveMLNetModel(string mlnetModelFilePath)
41 | {
42 | // Save/persist the model to a .ZIP file to be loaded by the PredictionEnginePool
43 | mlContext.Model.Save(mlModel, null, mlnetModelFilePath);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/OnnxObjectDetection/OnnxObjectDetection.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.1
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/OnnxObjectDetection/OnnxOutputParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Drawing;
4 | using System.Linq;
5 |
6 | namespace OnnxObjectDetection
7 | {
8 | public class OnnxOutputParser
9 | {
10 | class BoundingBoxPrediction : BoundingBoxDimensions
11 | {
12 | public float Confidence { get; set; }
13 | }
14 |
15 | ///
16 | /// The number of rows in the grid the image is divided into.
17 | ///
18 | public const int rowCount = 13;
19 | ///
20 | /// The number of columns in the grid the image is divided into.
21 | ///
22 | public const int columnCount = 13;
23 |
24 | ///
25 | /// The number of features contained within a box (x, y, height, width, confidence).
26 | ///
27 | public const int featuresPerBox = 5;
28 |
29 | ///
30 | /// Labels corresponding to the classes the onnx model can predict. For example, the
31 | /// Tiny YOLOv2 model included with this sample is trained to predict 20 different classes.
32 | ///
33 | private readonly string[] classLabels;
34 |
35 | ///
36 | /// Predetermined anchor offsets for the bounding boxes in a cell.
37 | ///
38 | private readonly (float x, float y)[] boxAnchors;
39 |
40 |
41 | public OnnxOutputParser(IOnnxModel onnxModel)
42 | {
43 | classLabels = onnxModel.Labels;
44 | boxAnchors = onnxModel.Anchors;
45 | }
46 |
47 | ///
48 | /// Applies the sigmoid function that outputs a number between 0 and 1.
49 | ///
50 | private float Sigmoid(float value)
51 | {
52 | var k = MathF.Exp(value);
53 | return k / (1.0f + k);
54 | }
55 |
56 | ///
57 | /// Normalizes an input vector into a probability distribution.
58 | ///
59 | private float[] Softmax(float[] classProbabilities)
60 | {
61 | var max = classProbabilities.Max();
62 | var exp = classProbabilities.Select(v => MathF.Exp(v - max));
63 | var sum = exp.Sum();
64 | return exp.Select(v => v / sum).ToArray();
65 | }
66 |
67 | ///
68 | /// Onnx outputst a tensor that has a shape of (for Tiny YOLOv2) 125x13x13. ML.NET flattens
69 | /// this multi-dimensional into a one-dimensional array. This method allows us to access a
70 | /// specific channel for a givin (x,y) cell position by calculating the offset into the array.
71 | ///
72 | private int GetOffset(int row, int column, int channel)
73 | {
74 | const int channelStride = rowCount * columnCount;
75 | return (channel * channelStride) + (column * columnCount) + row;
76 | }
77 |
78 | ///
79 | /// Extracts the bounding box features (x, y, height, width, confidence) method from the model
80 | /// output. The confidence value states how sure the model is that it has detected an object.
81 | /// We use the Sigmoid function to turn it that confidence into a percentage.
82 | ///
83 | private BoundingBoxPrediction ExtractBoundingBoxPrediction(float[] modelOutput, int row, int column, int channel)
84 | {
85 | return new BoundingBoxPrediction
86 | {
87 | X = modelOutput[GetOffset(row, column, channel++)],
88 | Y = modelOutput[GetOffset(row, column, channel++)],
89 | Width = modelOutput[GetOffset(row, column, channel++)],
90 | Height = modelOutput[GetOffset(row, column, channel++)],
91 | Confidence = Sigmoid(modelOutput[GetOffset(row, column, channel++)])
92 | };
93 | }
94 |
95 | ///
96 | /// The predicted x and y coordinates are relative to the location of the grid cell; we use
97 | /// the logistic sigmoid to constrain these coordinates to the range 0 - 1. Then we add the
98 | /// cell coordinates (0-12) and multiply by the number of pixels per grid cell (32).
99 | /// Now x/y represent the center of the bounding box in the original 416x416 image space.
100 | /// Additionally, the size (width, height) of the bounding box is predicted relative to the
101 | /// size of an "anchor" box. So we transform the width/weight into the original 416x416 image space.
102 | ///
103 | private BoundingBoxDimensions MapBoundingBoxToCell(int row, int column, int box, BoundingBoxPrediction boxDimensions)
104 | {
105 | const float cellWidth = ImageSettings.imageWidth / columnCount;
106 | const float cellHeight = ImageSettings.imageHeight / rowCount;
107 |
108 | var mappedBox = new BoundingBoxDimensions
109 | {
110 | X = (row + Sigmoid(boxDimensions.X)) * cellWidth,
111 | Y = (column + Sigmoid(boxDimensions.Y)) * cellHeight,
112 | Width = MathF.Exp(boxDimensions.Width) * cellWidth * boxAnchors[box].x,
113 | Height = MathF.Exp(boxDimensions.Height) * cellHeight * boxAnchors[box].y,
114 | };
115 |
116 | // The x,y coordinates from the (mapped) bounding box prediction represent the center
117 | // of the bounding box. We adjust them here to represent the top left corner.
118 | mappedBox.X -= mappedBox.Width / 2;
119 | mappedBox.Y -= mappedBox.Height / 2;
120 |
121 | return mappedBox;
122 | }
123 |
124 | ///
125 | /// Extracts the class predictions for the bounding box from the model output using the
126 | /// GetOffset method and turns them into a probability distribution using the Softmax method.
127 | ///
128 | public float[] ExtractClassProbabilities(float[] modelOutput, int row, int column, int channel, float confidence)
129 | {
130 | var classProbabilitiesOffset = channel + featuresPerBox;
131 | float[] classProbabilities = new float[classLabels.Length];
132 | for (int classProbability = 0; classProbability < classLabels.Length; classProbability++)
133 | classProbabilities[classProbability] = modelOutput[GetOffset(row, column, classProbability + classProbabilitiesOffset)];
134 | return Softmax(classProbabilities).Select(p => p * confidence).ToArray();
135 | }
136 |
137 | ///
138 | /// IoU (Intersection over union) measures the overlap between 2 boundaries. We use that to
139 | /// measure how much our predicted boundary overlaps with the ground truth (the real object
140 | /// boundary). In some datasets, we predefine an IoU threshold (say 0.5) in classifying
141 | /// whether the prediction is a true positive or a false positive. This method filters
142 | /// overlapping bounding boxes with lower probabilities.
143 | ///
144 | private float IntersectionOverUnion(RectangleF boundingBoxA, RectangleF boundingBoxB)
145 | {
146 | var areaA = boundingBoxA.Width * boundingBoxA.Height;
147 | var areaB = boundingBoxB.Width * boundingBoxB.Height;
148 |
149 | if (areaA <= 0 || areaB <= 0)
150 | return 0;
151 |
152 | var minX = MathF.Max(boundingBoxA.Left, boundingBoxB.Left);
153 | var minY = MathF.Max(boundingBoxA.Top, boundingBoxB.Top);
154 | var maxX = MathF.Min(boundingBoxA.Right, boundingBoxB.Right);
155 | var maxY = MathF.Min(boundingBoxA.Bottom, boundingBoxB.Bottom);
156 |
157 | var intersectionArea = MathF.Max(maxY - minY, 0) * MathF.Max(maxX - minX, 0);
158 |
159 | return intersectionArea / (areaA + areaB - intersectionArea);
160 | }
161 |
162 | public List ParseOutputs(float[] modelOutput, float probabilityThreshold = .3f)
163 | {
164 | var boxes = new List();
165 |
166 | for (int row = 0; row < rowCount; row++)
167 | {
168 | for (int column = 0; column < columnCount; column++)
169 | {
170 | for (int box = 0; box < boxAnchors.Length; box++)
171 | {
172 | var channel = box * (classLabels.Length + featuresPerBox);
173 |
174 | var boundingBoxPrediction = ExtractBoundingBoxPrediction(modelOutput, row, column, channel);
175 |
176 | var mappedBoundingBox = MapBoundingBoxToCell(row, column, box, boundingBoxPrediction);
177 |
178 | if (boundingBoxPrediction.Confidence < probabilityThreshold)
179 | continue;
180 |
181 | float[] classProbabilities = ExtractClassProbabilities(modelOutput, row, column, channel, boundingBoxPrediction.Confidence);
182 |
183 | var (topProbability, topIndex) = classProbabilities.Select((probability, index) => (Score: probability, Index: index)).Max();
184 |
185 | if (topProbability < probabilityThreshold)
186 | continue;
187 |
188 | boxes.Add(new BoundingBox
189 | {
190 | Dimensions = mappedBoundingBox,
191 | Confidence = topProbability,
192 | Label = classLabels[topIndex],
193 | BoxColor = BoundingBox.GetColor(topIndex)
194 | });
195 | }
196 | }
197 | }
198 | return boxes;
199 | }
200 |
201 | public List FilterBoundingBoxes(List boxes, int limit, float iouThreshold)
202 | {
203 | var results = new List();
204 | var filteredBoxes = new bool[boxes.Count];
205 | var sortedBoxes = boxes.OrderByDescending(b => b.Confidence).ToArray();
206 |
207 | for (int i = 0; i < boxes.Count; i++)
208 | {
209 | if (filteredBoxes[i])
210 | continue;
211 |
212 | results.Add(sortedBoxes[i]);
213 |
214 | if (results.Count >= limit)
215 | break;
216 |
217 | for (var j = i + 1; j < boxes.Count; j++)
218 | {
219 | if (filteredBoxes[j])
220 | continue;
221 |
222 | if (IntersectionOverUnion(sortedBoxes[i].Rect, sortedBoxes[j].Rect) > iouThreshold)
223 | filteredBoxes[j] = true;
224 |
225 | if (filteredBoxes.Count(b => b) <= 0)
226 | break;
227 | }
228 | }
229 | return results;
230 | }
231 | }
232 | }
233 |
--------------------------------------------------------------------------------