/// <summary> /// This will adjust the histograms in the data, ensuring that no gradient angle bin has a value greater than one. The histograms will be adjusted based upon the largest magnitude /// with a block of histograms - all magnitudes will be divided by the greatest magnitude of any angle within that block, such that no value anywhere will be greater than one /// </summary> public DataRectangle <HistogramOfGradient> Normalise(DataRectangle <HistogramOfGradient> hogs) { if (hogs == null) { throw new ArgumentNullException(nameof(hogs)); } if ((hogs.Width < _blockSize) || (hogs.Height < _blockSize)) { throw new ArgumentException($"too little data ({hogs.Width}x{hogs.Height}) for specified block size ({_blockSize})"); } return(hogs.Transform((hog, point) => { var x2 = Math.Min(point.X + _blockSize, hogs.Width); var x1 = x2 - _blockSize; var y2 = Math.Min(point.Y + _blockSize, hogs.Height); var y1 = y2 - _blockSize; var maxMagnitudeWithinBlock = hogs .Enumerate((p, h) => (p.X >= x1) && (p.X <= x2) && (p.Y >= y1) && (p.Y <= y2)) .Max(pointAndHog => pointAndHog.Item2.GreatestMagnitude); return (maxMagnitudeWithinBlock == 0) // TODO: Need tests, particularly around this sort of thing ? hog.Normalise() : hog.Multiply(1 / maxMagnitudeWithinBlock); })); }
/// <summary> /// This will adjust the histograms throughout the entire data, ensuring that the gradient angle bin with the largest magnitude is set to one and that all other magnitudes /// are adjust proportionally /// </summary> public static DataRectangle <HistogramOfGradient> Normalise(DataRectangle <HistogramOfGradient> hogs) { if (hogs == null) { throw new ArgumentNullException(nameof(hogs)); } var maxMagnitude = hogs.Enumerate().Select(pointAndHistogram => pointAndHistogram.Item2).Max(histogram => histogram.GreatestMagnitude); return((maxMagnitude == 0) // TODO: Need tests, particularly around this sort of thing ? hogs.Transform(hog => hog.Normalise()) : hogs.Transform(hog => hog.Multiply(1 / maxMagnitude))); }
private static HistogramOfGradient GenerateHistogram(DataRectangle <Vector> gradients) { if (gradients == null) { throw new ArgumentNullException(nameof(gradients)); } // The HoG description here is very helpful: http://mccormickml.com/2013/05/09/hog-person-detector-tutorial/ (in particular, the diagram of the histogram // and the statements "For each gradient vector, it’s contribution to the histogram is given by the magnitude of the vector" and "We split the contribution // between the two closest bins" var bins = new double[180 / 20]; foreach (var gradient in gradients.Enumerate().Select(pointAndValue => pointAndValue.Item2)) { int bin0, bin1; double fractionForBin0; if (gradient.Angle <= 10) { // If we're all the way over on the left hand side of the graph then we split between the first and last bins since -1 degrees is the same 179. // At least 50% of it will go into bin0 since we're closer to the centre of bin0 than bin{last}. bin0 = 0; bin1 = bins.Length - 1; var distanceIntoCurrentBin = gradient.Angle; fractionForBin0 = 0.5 + (0.25 * (distanceIntoCurrentBin / 20)); } else if (gradient.Angle >= 170) { // If we're all the way over on the right hand side of the graph then we split between the last and first bins since 181 degrees is the same 1. // At least 50% of it will go into bin{last} since we're closer to the centre of bin{last} than bin{0}. bin0 = bins.Length - 1; bin1 = 0; var distanceIntoCurrentBin = 180 - gradient.Angle; fractionForBin0 = 0.5 + (0.25 * (distanceIntoCurrentBin / 20)); } else { // When we're somewhere in the middle, subtracting half the bin size and then dividing by the bin size will get a fractional value that is between // the two bin indexes that the value should be distributed across - eg. if bin size is 20 and the angle is 105 then subtract (20/2)=10 from 105 to // get 95 and then divide by 20 to get 4.75, this means that bins[4] and bins[5] should be updated. If an integer value is returned then the value // is assigned to a single bin and not spread between two. bin0 = (int)Math.Floor((double)(gradient.Angle - 10) / 20); bin1 = (int)Math.Ceiling((double)(gradient.Angle - 10) / 20); var bin1Centre = (bin1 * 20) + 10; fractionForBin0 = (double)(bin1Centre - gradient.Angle) / 20; // The further from bin1 it is, the more than bin0 gets } bins[bin0] += (gradient.Magnitude * fractionForBin0); bins[bin1] += (gradient.Magnitude * (1 - fractionForBin0)); } return(new HistogramOfGradient(bins[0], bins[1], bins[2], bins[3], bins[4], bins[5], bins[6], bins[7], bins[8])); }
private static DataRectangle <RGB> CorrectZeroResponse(DataRectangle <RGB> values) { if (values == null) { throw new ArgumentNullException(nameof(values)); } // Get the smallest value of any RGB component var smallestValue = values .Enumerate() .Select(point => point.Item2) .SelectMany(colour => new[] { colour.R, colour.G, colour.B }) .Min(); // Subtract this from every RGB component return(values.Transform(value => new RGB((byte)(value.R - smallestValue), (byte)(value.G - smallestValue), (byte)(value.B - smallestValue)))); }
private IEnumerable <Rectangle> IdentifyFacesFromSkinMask(DataRectangle <bool> skinMask) { if (skinMask == null) { throw new ArgumentNullException(nameof(skinMask)); } // Identify potential objects from positive image (build a list of all skin points, take the first one and flood fill from it - recording the results as one object // and remove all points from the list, then do the same for the next skin point until there are none left) var skinPoints = new HashSet <Point>( skinMask.Enumerate((point, isMasked) => isMasked).Select(point => point.Item1) ); var scale = _config.CalculateScale(skinMask.Width, skinMask.Height); var skinObjects = new List <Point[]>(); while (skinPoints.Any()) { var currentPoint = skinPoints.First(); var pointsInObject = TryToGetPointsInObject(skinMask, currentPoint, new Rectangle(0, 0, skinMask.Width, skinMask.Height)).ToArray(); foreach (var point in pointsInObject) { skinPoints.Remove(point); } skinObjects.Add(pointsInObject); } skinObjects = skinObjects.Where(skinObject => skinObject.Length >= (64 * scale)).ToList(); // Ignore any very small regions if (_config.SaveProgressImages) { var skinMaskPreviewImage = new Bitmap(skinMask.Width, skinMask.Height); var skinObjectPreviewColours = new[] { new RGB(255, 0, 0), new RGB(0, 255, 0), new RGB(0, 0, 255), new RGB(128, 128, 0), new RGB(0, 128, 128), new RGB(128, 0, 128) }; var allSkinObjectPoints = skinObjects.Select((o, i) => new { Points = new HashSet <Point>(o), Colour = skinObjectPreviewColours[i % skinObjectPreviewColours.Length] }).ToArray(); skinMaskPreviewImage.SetRGB( skinMask.Transform((isSkin, point) => { var firstObject = allSkinObjectPoints.FirstOrDefault(o => o.Points.Contains(point)); return((firstObject == null) ? new RGB(0, 0, 0) : firstObject.Colour); }) ); skinMaskPreviewImage.Save("SkinObjects.png"); } // Look for any fully enclosed holes in each skin object (do this by flood filling from negative points and ignoring any where the fill gets to the edges of object) var boundsForSkinObjects = new List <Rectangle>(); foreach (var skinObject in skinObjects) { var xValues = skinObject.Select(p => p.X).ToArray(); var yValues = skinObject.Select(p => p.Y).ToArray(); var left = xValues.Min(); var top = yValues.Min(); var skinObjectBounds = new Rectangle(left, top, width: (xValues.Max() - left) + 1, height: (yValues.Max() - top) + 1); var negativePointsInObject = new HashSet <Point>( skinMask.Enumerate((point, isMasked) => !isMasked && skinObjectBounds.Contains(point)).Select(point => point.Item1) ); while (negativePointsInObject.Any()) { var currentPoint = negativePointsInObject.First(); var pointsInFilledNegativeSpace = TryToGetPointsInObject(skinMask, currentPoint, skinObjectBounds).ToArray(); foreach (var point in pointsInFilledNegativeSpace) { negativePointsInObject.Remove(point); } if (pointsInFilledNegativeSpace.Any(p => (p.X == skinObjectBounds.Left) || (p.X == (skinObjectBounds.Right - 1)) || (p.Y == skinObjectBounds.Top) || (p.Y == (skinObjectBounds.Bottom - 1)))) { continue; // Ignore any negative regions that are not fully enclosed within the skin mask } if (pointsInFilledNegativeSpace.Length <= scale) { continue; // Ignore any very small regions (likely anomalies) } boundsForSkinObjects.Add(skinObjectBounds); // Found a non-negligible fully-enclosed hole break; } } return(boundsForSkinObjects); }