public bool IsFace(Bitmap image) { if (image == null) { throw new ArgumentNullException(nameof(image)); } using (var windowedImageForFeatureExtraction = image.ExtractImageSectionAndResize(new Rectangle(new Point(0, 0), image.Size), new Size(_sampleWidth, _sampleHeight))) { return(_svm.Decide( FeatureExtractor.GetFor(windowedImageForFeatureExtraction, _blockSize, optionalHogPreviewImagePath: null, normaliser: _normaliser).ToArray() )); } }
public static IClassifyPotentialFaces TrainFromCaltechData( DirectoryInfo caltechWebFacesSourceImageFolder, FileInfo groundTruthTextFile, int sampleWidth, int sampleHeight, int blockSize, int minimumNumberOfImagesToTrainWith, Normaliser normaliser, Action <string> logger) { if (caltechWebFacesSourceImageFolder == null) { throw new ArgumentNullException(nameof(caltechWebFacesSourceImageFolder)); } if (groundTruthTextFile == null) { throw new ArgumentNullException(nameof(groundTruthTextFile)); } if (sampleWidth <= 0) { throw new ArgumentOutOfRangeException(nameof(sampleWidth)); } if (sampleHeight <= 0) { throw new ArgumentOutOfRangeException(nameof(sampleHeight)); } if (blockSize <= 0) { throw new ArgumentOutOfRangeException(nameof(blockSize)); } if (minimumNumberOfImagesToTrainWith <= 0) { throw new ArgumentOutOfRangeException(nameof(minimumNumberOfImagesToTrainWith)); } if (normaliser == null) { throw new ArgumentNullException(nameof(normaliser)); } if (logger == null) { throw new ArgumentNullException(nameof(logger)); } var timer = Stopwatch.StartNew(); var trainingDataOfHogsAndIsFace = new List <Tuple <double[], bool> >(); var numberOfImagesThatLastProgressMessageWasShownAt = 0; const int numberOfImagesToProcessBeforeShowingUpdateMessage = 20; foreach (var imagesFromSingleReferenceImage in ExtractPositiveAndNegativeTrainingDataFromCaltechWebFaces(sampleWidth, sampleHeight, groundTruthTextFile, caltechWebFacesSourceImageFolder)) { // We want to train using the same number of positive images as negative images. It's possible that we were unable to extract as many non-face regions from the source // image as we did face regions. In this case, discount the image and move on to the next one. var numberOfPositiveImagesExtracted = imagesFromSingleReferenceImage.Count(imageAndIsFaceDecision => imageAndIsFaceDecision.Item2); var numberOfNegativeImagesExtracted = imagesFromSingleReferenceImage.Count(imageAndIsFaceDecision => !imageAndIsFaceDecision.Item2); if (numberOfPositiveImagesExtracted != numberOfNegativeImagesExtracted) { foreach (var image in imagesFromSingleReferenceImage.Select(imageAndIsFaceDecision => imageAndIsFaceDecision.Item1)) { image.Dispose(); } continue; } foreach (var imageAndIsFaceDecision in imagesFromSingleReferenceImage) { var image = imageAndIsFaceDecision.Item1; var isFace = imageAndIsFaceDecision.Item2; trainingDataOfHogsAndIsFace.Add(Tuple.Create( FeatureExtractor.GetFor(image, blockSize, optionalHogPreviewImagePath: null, normaliser: normaliser).ToArray(), isFace )); image.Dispose(); } var approximateNumberOfImagesProcessed = (int)Math.Floor((double)trainingDataOfHogsAndIsFace.Count / numberOfImagesToProcessBeforeShowingUpdateMessage) * numberOfImagesToProcessBeforeShowingUpdateMessage; if (approximateNumberOfImagesProcessed > numberOfImagesThatLastProgressMessageWasShownAt) { logger("Processed " + approximateNumberOfImagesProcessed + " images"); numberOfImagesThatLastProgressMessageWasShownAt = approximateNumberOfImagesProcessed; } if (trainingDataOfHogsAndIsFace.Count >= minimumNumberOfImagesToTrainWith) { break; } } if (trainingDataOfHogsAndIsFace.Count < minimumNumberOfImagesToTrainWith) { throw new Exception($"After loaded all data, there are only {trainingDataOfHogsAndIsFace.Count} training images but {minimumNumberOfImagesToTrainWith} were requested"); } logger("Time to load image data: " + timer.Elapsed.TotalSeconds.ToString("0.00") + "s"); timer.Restart(); var smo = new SequentialMinimalOptimization <Linear>(); var inputs = trainingDataOfHogsAndIsFace.Select(dataAndResult => dataAndResult.Item1).ToArray(); var outputs = trainingDataOfHogsAndIsFace.Select(dataAndResult => dataAndResult.Item2).ToArray(); var svm = smo.Learn(inputs, outputs); logger("Time to teach SVM: " + timer.Elapsed.TotalSeconds.ToString("0.00") + "s"); timer.Restart(); // The SVM kernel contains lots of information from the training process which can be reduced down (from the Compress method's summary documentation: "If this machine has // a linear kernel, compresses all support vectors into a single parameter vector)". This additional data is of no use to use so we can safely get rid of it - this will // be beneficial if we decide to persist the trained SVM since they will be less data to serialise. svm.Compress(); logger("Time to compress SVM: " + timer.Elapsed.TotalSeconds.ToString("0.00") + "s"); timer.Restart(); var predicted = svm.Decide(inputs); var error = new ZeroOneLoss(outputs).Loss(predicted); if (error > 0) { logger("*** Generated SVM has non-zero error against training data: " + error); } logger("Time to test SVM against training data: " + timer.Elapsed.TotalSeconds.ToString("0.00") + "s"); timer.Restart(); return(new SvmClassifier(svm, sampleWidth, sampleHeight, blockSize, normaliser)); }