diff --git a/FaceRecognitionDotNet.sln b/FaceRecognitionDotNet.sln index 4018445..ab4ad72 100644 --- a/FaceRecognitionDotNet.sln +++ b/FaceRecognitionDotNet.sln @@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HeadPoseTraining", "tools\H EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HeadPoseEstimationDemo", "examples\HeadPoseEstimationDemo\HeadPoseEstimationDemo.csproj", "{8540B212-5605-4402-BAE4-BEFEBAAD9F97}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkEndToEnd", "examples\BenchmarkEndToEnd\BenchmarkEndToEnd.csproj", "{F26A2088-EB8B-43F0-8F78-9A2BCE514367}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -113,6 +115,10 @@ Global {8540B212-5605-4402-BAE4-BEFEBAAD9F97}.Debug|Any CPU.Build.0 = Debug|Any CPU {8540B212-5605-4402-BAE4-BEFEBAAD9F97}.Release|Any CPU.ActiveCfg = Release|Any CPU {8540B212-5605-4402-BAE4-BEFEBAAD9F97}.Release|Any CPU.Build.0 = Release|Any CPU + {F26A2088-EB8B-43F0-8F78-9A2BCE514367}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F26A2088-EB8B-43F0-8F78-9A2BCE514367}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F26A2088-EB8B-43F0-8F78-9A2BCE514367}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F26A2088-EB8B-43F0-8F78-9A2BCE514367}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -134,6 +140,7 @@ Global {13DDB1E7-EF71-48BA-A1E7-365C94783709} = {FEEAC07F-70D7-4C12-B92C-153CEE0F2539} {BB6A1F98-DEF9-475C-A69A-82FE518D1E9E} = {8C8838E0-B002-426F-9B25-4C1F65A6D33D} {8540B212-5605-4402-BAE4-BEFEBAAD9F97} = {FEEAC07F-70D7-4C12-B92C-153CEE0F2539} + {F26A2088-EB8B-43F0-8F78-9A2BCE514367} = {FEEAC07F-70D7-4C12-B92C-153CEE0F2539} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4D44C572-D749-4A76-A199-8C598A08AE8A} diff --git a/examples/BenchmarkEndToEnd/BenchmarkEndToEnd.csproj b/examples/BenchmarkEndToEnd/BenchmarkEndToEnd.csproj new file mode 100644 index 0000000..f7fe68e --- /dev/null +++ b/examples/BenchmarkEndToEnd/BenchmarkEndToEnd.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp2.0 + Takuya Takeuchi + Example of FaceRecognitionDotNet + + + + + + + + + + + diff --git a/examples/BenchmarkEndToEnd/Program.cs b/examples/BenchmarkEndToEnd/Program.cs new file mode 100644 index 0000000..5b2d478 --- /dev/null +++ b/examples/BenchmarkEndToEnd/Program.cs @@ -0,0 +1,141 @@ +/* + * This sample program is ported by C# from https://github.com/ageitgey/face_recognition/blob/master/examples/benchmark.py. +*/ + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using FaceRecognitionDotNet; +using Microsoft.Extensions.CommandLineUtils; + +namespace BenchmarkEndToEnd +{ + + internal class Program + { + + #region Fields + + private static FaceRecognition _FaceRecognition; + + private static bool _UseCnn = false; + + #endregion + + #region Methods + + private static void Main(string[] args) + { + var app = new CommandLineApplication(false); + app.Name = nameof(BenchmarkEndToEnd); + app.Description = "The program for measure face encoding performance"; + app.HelpOption("-h|--help"); + + var modelsOption = app.Option("-m|--model", "model files directory path", CommandOptionType.SingleValue); + var cnnOption = app.Option("-c|--cnn", "use cnn", CommandOptionType.NoValue); + + app.OnExecute(() => + { + if (!modelsOption.HasValue()) + { + app.ShowHelp(); + return -1; + } + + var directory = modelsOption.Value(); + if (!Directory.Exists(directory)) + { + app.ShowHelp(); + return -1; + } + + _UseCnn = cnnOption.HasValue(); + + _FaceRecognition = FaceRecognition.Create(directory); + + var testImages = new[] + { + "obama-240p.jpg", + "obama-480p.jpg", + "obama-720p.jpg", + "obama-1080p.jpg" + }; + + Console.WriteLine("Benchmarks"); + Console.WriteLine(); + + foreach (var image in testImages) + { + var size = image.Split('-')[1].Split('.')[0]; + Console.WriteLine($"Timings at {size}:"); + + var faceLocations = RunTest(image, SetupLocateFaces, TestEndToEnd); + Console.WriteLine($" - Face locations, landmark, encoding, distance: {faceLocations.Item1:F4}s ({faceLocations.Item2:F2} fps)"); + Console.WriteLine(); + } + + return 0; + }); + + app.Execute(args); + } + + #region Helpers + + private static Tuple RunTest(string path, Func setup, Action test, int iterationsPerTest = 5, int testsToRun = 10, bool useCnn = false) + { + var image = setup(path); + + var iteration = new Func(() => + { + var sw = new Stopwatch(); + sw.Start(); + for (var count = 0; count < iterationsPerTest; count++) + test(image); + sw.Stop(); + + return sw.ElapsedMilliseconds; + }); + + var fastestExecution = Enumerable.Repeat(0, testsToRun).Select(i => iteration()).Min(); + var executionTime = fastestExecution / 1000 / iterationsPerTest; + var fps = 1.0 / executionTime; + + (image as IDisposable)?.Dispose(); + + return new Tuple(executionTime, fps); + } + + private static Image SetupLocateFaces(string path) + { + return FaceRecognition.LoadImageFile(path); + } + + private static void TestEndToEnd(Image image) + { + var model = _UseCnn ? Model.Cnn : Model.Hog; + var faceLocations = _FaceRecognition.FaceLocations(image, model: model); + var faceLocationCount = faceLocations.Count(); + + var faceLandmarks = _FaceRecognition.FaceLandmark(image, faceLocations, model: model); + var faceLandmarkCount = faceLandmarks.Count(); + + var encoding = _FaceRecognition.FaceEncodings(image, faceLocations, model: model); + var faceEncodingCount = encoding.Count(); + + // it could do matching for 1 time + foreach (var faceEncoding in encoding) + FaceRecognition.FaceDistance(faceEncoding, faceEncoding); + + foreach (var faceEncoding in encoding) + faceEncoding.Dispose(); + } + + #endregion + + #endregion + + } + +} diff --git a/examples/BenchmarkEndToEnd/README.md b/examples/BenchmarkEndToEnd/README.md new file mode 100644 index 0000000..86c065a --- /dev/null +++ b/examples/BenchmarkEndToEnd/README.md @@ -0,0 +1,73 @@ +# Benchmark + +This example measures performance of calculating for face encodings. +This sample program is ported by C# from https://github.com/ageitgey/face_recognition/blob/master/examples/benchmark.py. + +## How to use? + +## 1. Preparation + +This sample requires test image and model files. + +## 2. Build + +1. Open command prompt and change to <Benchmark_dir> +1. Type the following command +```` +$ dotnet remove reference ../../src/FaceRecognitionDotNet\FaceRecognitionDotNet.csproj +$ dotnet add package FaceRecognitionDotNet +$ dotnet build -c Release +```` +2. Copy ***DlibDotNet.dll***, ***DlibDotNet.Native.dll*** and ***DlibDotNet.Dnn.dll*** to output directory; <Benchmark_dir>\bin\Release\netcoreapp2.0. + * if you use FaceRecognitionDotNet with CUDA, you must copy also cuda libraries. + +## 3. Run + +1. Open command prompt and change to <Benchmark_dir> +1. Type the following sample command +```` +$ dotnet run -c Release -- "-m=models" +Benchmarks + +Timings at 240p: + - Face locations: 0.0268s (37.31 fps) + - Face landmarks: 0.0014s (714.29 fps) + - Encode face (inc. landmarks): 0.0210s (47.62 fps) + - End-to-end: 0.0484s (20.66 fps) + +Timings at 480p: + - Face locations: 0.1068s (9.36 fps) + - Face landmarks: 0.0014s (714.29 fps) + - Encode face (inc. landmarks): 0.0202s (49.50 fps) + - End-to-end: 0.1308s (7.65 fps) + +Timings at 720p: + - Face locations: 0.2416s (4.14 fps) + - Face landmarks: 0.0014s (714.29 fps) + - Encode face (inc. landmarks): 0.0206s (48.54 fps) + - End-to-end: 0.2700s (3.70 fps) + +Timings at 1080p: + - Face locations: 0.5430s (1.84 fps) + - Face landmarks: 0.0016s (625.00 fps) + - Encode face (inc. landmarks): 0.0206s (48.54 fps) + - End-to-end: 0.5774s (1.73 fps) +```` + +## 4. Parameters + +This program support the following argument and option. + +### Argument + +|Argument|Description| +|:---|:---| +|-m\|--model|Directory path includes model files| +|-c\|--cnn|Use Cnn| + +## 5. Other + +### Why is Encode face too slow? + +The reason ***face_recognition*** can achieve high performance is using ***Intel Math Kernel Library***. +If you can use Intel Math Kernel Library, you can build ***DlibDotNet.Native.Dnn*** by linking Intel Math Kernel Library. \ No newline at end of file diff --git a/examples/BenchmarkEndToEnd/obama-1080p.jpg b/examples/BenchmarkEndToEnd/obama-1080p.jpg new file mode 100644 index 0000000..a244779 Binary files /dev/null and b/examples/BenchmarkEndToEnd/obama-1080p.jpg differ diff --git a/examples/BenchmarkEndToEnd/obama-240p.jpg b/examples/BenchmarkEndToEnd/obama-240p.jpg new file mode 100644 index 0000000..c4a947a Binary files /dev/null and b/examples/BenchmarkEndToEnd/obama-240p.jpg differ diff --git a/examples/BenchmarkEndToEnd/obama-480p.jpg b/examples/BenchmarkEndToEnd/obama-480p.jpg new file mode 100644 index 0000000..78837ef Binary files /dev/null and b/examples/BenchmarkEndToEnd/obama-480p.jpg differ diff --git a/examples/BenchmarkEndToEnd/obama-720p.jpg b/examples/BenchmarkEndToEnd/obama-720p.jpg new file mode 100644 index 0000000..d06cec3 Binary files /dev/null and b/examples/BenchmarkEndToEnd/obama-720p.jpg differ diff --git a/nuget/.gitignore b/nuget/.gitignore new file mode 100644 index 0000000..1c2f433 --- /dev/null +++ b/nuget/.gitignore @@ -0,0 +1 @@ +tmp \ No newline at end of file diff --git a/nuget/ExtractNupkgToArtifacts.ps1 b/nuget/ExtractNupkgToArtifacts.ps1 new file mode 100644 index 0000000..f1ba255 --- /dev/null +++ b/nuget/ExtractNupkgToArtifacts.ps1 @@ -0,0 +1,48 @@ +#*************************************** +#Arguments +#%1: Version of Release (1.2.3.0) +#*************************************** +Param([Parameter( + Mandatory=$True, + Position = 1 + )][string] + $Version +) + +$PublishTargets = @{ "FaceRecognitionDotNet"="cpu"; + "FaceRecognitionDotNet.CUDA92"="cuda-92"; + "FaceRecognitionDotNet.CUDA100"="cuda-100"; + "FaceRecognitionDotNet.CUDA101"="cuda-101"; + "FaceRecognitionDotNet.CUDA102"="cuda-102"; + "FaceRecognitionDotNet.CUDA110"="cuda-110"; + "FaceRecognitionDotNet.CUDA111"="cuda-111"; + "FaceRecognitionDotNet.CUDA112"="cuda-112"; + "FaceRecognitionDotNet.MKL"="mkl"; + } + +$Token = $env:FaceRecognitionDotNetNugetToken +if ([string]::IsNullOrWhitespace($Token)) +{ + Write-Host "nuget token is missing" -ForegroundColor Red + exit +} + +# Precheck whether all package is present +foreach ($key in $PublishTargets.keys) +{ + $value = $PublishTargets[$key] + + $Package = Join-Path $PSScriptRoot "${key}.${Version}.nupkg" + if (!(Test-Path ${Package})) + { + Write-Host "${Package} is missing" -ForegroundColor Red + exit + } + + Expand-Archive -Path "${Package}" -DestinationPath tmp + $runtime = Join-Path tmp runtimes + $artifacts = Join-Path artifacts ${value} | ` + Join-Path -ChildPath runtimes + Copy-Item "${runtime}/*" "${artifacts}" -Recurse -Force + Remove-Item tmp -Recurse -Force +} \ No newline at end of file diff --git a/nuget/nuspec/FaceRecognitionDotNet.CPU.nuspec b/nuget/nuspec/FaceRecognitionDotNet.CPU.nuspec index a6143c9..80b7f44 100644 --- a/nuget/nuspec/FaceRecognitionDotNet.CPU.nuspec +++ b/nuget/nuspec/FaceRecognitionDotNet.CPU.nuspec @@ -2,7 +2,7 @@ FaceRecognitionDotNet - 1.3.0.4 + 1.3.0.5 FaceRecognitionDotNet Takuya Takeuchi Takuya Takeuchi diff --git a/nuget/nuspec/FaceRecognitionDotNet.CUDA100.nuspec b/nuget/nuspec/FaceRecognitionDotNet.CUDA100.nuspec index 6793192..2e71422 100644 --- a/nuget/nuspec/FaceRecognitionDotNet.CUDA100.nuspec +++ b/nuget/nuspec/FaceRecognitionDotNet.CUDA100.nuspec @@ -2,7 +2,7 @@ FaceRecognitionDotNet.CUDA100 - 1.3.0.4 + 1.3.0.5 FaceRecognitionDotNet for CUDA 10.0 Takuya Takeuchi Takuya Takeuchi diff --git a/nuget/nuspec/FaceRecognitionDotNet.CUDA101.nuspec b/nuget/nuspec/FaceRecognitionDotNet.CUDA101.nuspec index 8abb608..7871ac0 100644 --- a/nuget/nuspec/FaceRecognitionDotNet.CUDA101.nuspec +++ b/nuget/nuspec/FaceRecognitionDotNet.CUDA101.nuspec @@ -2,7 +2,7 @@ FaceRecognitionDotNet.CUDA101 - 1.3.0.4 + 1.3.0.5 FaceRecognitionDotNet for CUDA 10.1 Takuya Takeuchi Takuya Takeuchi diff --git a/nuget/nuspec/FaceRecognitionDotNet.CUDA102.nuspec b/nuget/nuspec/FaceRecognitionDotNet.CUDA102.nuspec index 6b51ede..667b212 100644 --- a/nuget/nuspec/FaceRecognitionDotNet.CUDA102.nuspec +++ b/nuget/nuspec/FaceRecognitionDotNet.CUDA102.nuspec @@ -2,7 +2,7 @@ FaceRecognitionDotNet.CUDA102 - 1.3.0.4 + 1.3.0.5 FaceRecognitionDotNet for CUDA 10.2 Takuya Takeuchi Takuya Takeuchi diff --git a/nuget/nuspec/FaceRecognitionDotNet.CUDA110.nuspec b/nuget/nuspec/FaceRecognitionDotNet.CUDA110.nuspec index 5666957..1dc7e67 100644 --- a/nuget/nuspec/FaceRecognitionDotNet.CUDA110.nuspec +++ b/nuget/nuspec/FaceRecognitionDotNet.CUDA110.nuspec @@ -2,7 +2,7 @@ FaceRecognitionDotNet.CUDA110 - 1.3.0.4 + 1.3.0.5 FaceRecognitionDotNet for CUDA 11.0 Takuya Takeuchi Takuya Takeuchi diff --git a/nuget/nuspec/FaceRecognitionDotNet.CUDA111.nuspec b/nuget/nuspec/FaceRecognitionDotNet.CUDA111.nuspec index bda0ab5..bcfcd00 100644 --- a/nuget/nuspec/FaceRecognitionDotNet.CUDA111.nuspec +++ b/nuget/nuspec/FaceRecognitionDotNet.CUDA111.nuspec @@ -2,7 +2,7 @@ FaceRecognitionDotNet.CUDA111 - 1.3.0.4 + 1.3.0.5 FaceRecognitionDotNet for CUDA 11.1 Takuya Takeuchi Takuya Takeuchi diff --git a/nuget/nuspec/FaceRecognitionDotNet.CUDA112.nuspec b/nuget/nuspec/FaceRecognitionDotNet.CUDA112.nuspec index b84d271..212ef1a 100644 --- a/nuget/nuspec/FaceRecognitionDotNet.CUDA112.nuspec +++ b/nuget/nuspec/FaceRecognitionDotNet.CUDA112.nuspec @@ -2,7 +2,7 @@ FaceRecognitionDotNet.CUDA112 - 1.3.0.4 + 1.3.0.5 FaceRecognitionDotNet for CUDA 11.2 Takuya Takeuchi Takuya Takeuchi diff --git a/nuget/nuspec/FaceRecognitionDotNet.CUDA92.nuspec b/nuget/nuspec/FaceRecognitionDotNet.CUDA92.nuspec index a9b9fc1..5193ad9 100644 --- a/nuget/nuspec/FaceRecognitionDotNet.CUDA92.nuspec +++ b/nuget/nuspec/FaceRecognitionDotNet.CUDA92.nuspec @@ -2,7 +2,7 @@ FaceRecognitionDotNet.CUDA92 - 1.3.0.4 + 1.3.0.5 FaceRecognitionDotNet for CUDA 9.2 Takuya Takeuchi Takuya Takeuchi diff --git a/nuget/nuspec/FaceRecognitionDotNet.MKL.nuspec b/nuget/nuspec/FaceRecognitionDotNet.MKL.nuspec index 32df1f7..94dd67c 100644 --- a/nuget/nuspec/FaceRecognitionDotNet.MKL.nuspec +++ b/nuget/nuspec/FaceRecognitionDotNet.MKL.nuspec @@ -2,7 +2,7 @@ FaceRecognitionDotNet.MKL - 1.3.0.4 + 1.3.0.5 FaceRecognitionDotNet for MKL Takuya Takeuchi Takuya Takeuchi diff --git a/src/FaceRecognitionDotNet/FaceRecognition.cs b/src/FaceRecognitionDotNet/FaceRecognition.cs index 5551bec..18dafdf 100644 --- a/src/FaceRecognitionDotNet/FaceRecognition.cs +++ b/src/FaceRecognitionDotNet/FaceRecognition.cs @@ -215,9 +215,11 @@ public IEnumerable BatchFaceLocations(IEnumerable images, int if (images == null) throw new ArgumentNullException(nameof(images)); + var results = new List(); + var imagesArray = images.ToArray(); if (!imagesArray.Any()) - yield break; + return results; var rawDetectionsBatched = this.RawFaceLocationsBatched(imagesArray, numberOfTimesToUpsample, batchSize).ToArray(); @@ -227,9 +229,11 @@ public IEnumerable BatchFaceLocations(IEnumerable images, int var faces = rawDetectionsBatched[index].ToArray(); var locations = faces.Select(rect => new Location(TrimBound(rect.Rect, image.Width, image.Height), rect.DetectionConfidence)).ToArray(); foreach (var face in faces) - face.Dispose(); - yield return locations; + face.Dispose(); + results.Add(locations); } + + return results; } /// @@ -276,11 +280,14 @@ public static IEnumerable CompareFaces(IEnumerable knownFace if (array.Any(encoding => encoding.IsDisposed)) throw new ObjectDisposedException($"{nameof(knownFaceEncodings)} contains disposed object."); + var results = new List(); if (array.Length == 0) - yield break; + return results; foreach (var faceEncoding in array) - yield return FaceDistance(faceEncoding, faceEncodingToCheck) <= tolerance; + results.Add(FaceDistance(faceEncoding, faceEncodingToCheck) <= tolerance); + + return results; } /// @@ -322,6 +329,7 @@ public static IEnumerable CropFaces(Image image, IEnumerable lo image.ThrowIfDisposed(); + var results = new List(); foreach (var location in locations) { var rect = new Rectangle(location.Left, location.Top, location.Right, location.Bottom); @@ -340,16 +348,18 @@ public static IEnumerable CropFaces(Image image, IEnumerable lo { case Mode.Rgb: var rgb = image.Matrix as Matrix; - yield return new Image(DlibDotNet.Dlib.ExtractImage4Points(rgb, dPoint, width, height), - Mode.Rgb); + results.Add(new Image(DlibDotNet.Dlib.ExtractImage4Points(rgb, dPoint, width, height), + Mode.Rgb)); break; case Mode.Greyscale: var gray = image.Matrix as Matrix; - yield return new Image(DlibDotNet.Dlib.ExtractImage4Points(gray, dPoint, width, height), - Mode.Rgb); + results.Add(new Image(DlibDotNet.Dlib.ExtractImage4Points(gray, dPoint, width, height), + Mode.Rgb)); break; } } + + return results; } /// @@ -421,12 +431,15 @@ public static IEnumerable FaceDistances(IEnumerable faceEn if (array.Any(encoding => encoding.IsDisposed)) throw new ObjectDisposedException($"{nameof(faceEncodings)} contains disposed object."); + var results = new List(); if (array.Length == 0) - yield break; + return results; foreach (var faceEncoding in array) using (var diff = faceEncoding.Encoding - faceToCompare.Encoding) - yield return DlibDotNet.Dlib.Length(diff); + results.Add(DlibDotNet.Dlib.Length(diff)); + + return results; } /// @@ -460,12 +473,16 @@ public IEnumerable FaceEncodings(Image image, this.ThrowIfDisposed(); var rawLandmarks = this.RawFaceLandmarks(image, knownFaceLocation, predictorModel, model); + + var results = new List(); foreach (var landmark in rawLandmarks) { var ret = new FaceEncoding(FaceRecognitionModelV1.ComputeFaceDescriptor(this._FaceEncoder, image, landmark, numJitters)); landmark.Dispose(); - yield return ret; + results.Add(ret); } + + return results; } /// @@ -581,13 +598,16 @@ public IEnumerable FaceLocations(Image image, int numberOfTimesToUpsam image.ThrowIfDisposed(); this.ThrowIfDisposed(); + var results = new List(); foreach (var face in this.RawFaceLocations(image, numberOfTimesToUpsample, model)) { var ret = TrimBound(face.Rect, image.Width, image.Height); var confidence = face.DetectionConfidence; face.Dispose(); - yield return new Location(ret, confidence); + results.Add(new Location(ret, confidence)); } + + return results; } /// @@ -987,12 +1007,13 @@ private IEnumerable RawFaceLandmarks(Image faceImage, rects = faceLocations; } + var results = new List(); if (predictorModel == PredictorModel.Custom) { foreach (var rect in rects) { var ret = this._CustomFaceLandmarkDetector.Detect(faceImage, rect); - yield return ret; + results.Add(ret); } } else @@ -1008,9 +1029,11 @@ private IEnumerable RawFaceLandmarks(Image faceImage, foreach (var rect in rects) { var ret = posePredictor.Detect(faceImage.Matrix, new Rectangle(rect.Left, rect.Top, rect.Right, rect.Bottom)); - yield return ret; + results.Add(ret); } } + + return results; } private IEnumerable RawFaceLocations(Image faceImage, int numberOfTimesToUpsample = 1, Model model = Model.Hog) diff --git a/src/FaceRecognitionDotNet/FaceRecognitionDotNet.csproj b/src/FaceRecognitionDotNet/FaceRecognitionDotNet.csproj index d5da946..28e20be 100644 --- a/src/FaceRecognitionDotNet/FaceRecognitionDotNet.csproj +++ b/src/FaceRecognitionDotNet/FaceRecognitionDotNet.csproj @@ -6,7 +6,7 @@ © Takuya Takeuchi 2018-2021 Porting face_recognition (by Adam Geitgey) by C# - 1.3.0.4 + 1.3.0.5 https://github.com/takuya-takeuchi/FaceRecognitionDotNet .net machinelearning face-recognition