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));
        }