diff --git a/src/ImageProcessor.Playground/Program.cs b/src/ImageProcessor.Playground/Program.cs index 5c1b259d8..e403b59de 100644 --- a/src/ImageProcessor.Playground/Program.cs +++ b/src/ImageProcessor.Playground/Program.cs @@ -21,6 +21,7 @@ namespace ImageProcessor.PlayGround using ImageProcessor.Configuration; using ImageProcessor.Imaging; using ImageProcessor.Imaging.Filters.EdgeDetection; + using ImageProcessor.Imaging.Filters.ObjectDetection; using ImageProcessor.Imaging.Filters.Photo; using ImageProcessor.Imaging.Formats; using ImageProcessor.Processors; @@ -60,89 +61,92 @@ namespace ImageProcessor.PlayGround //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "gamma_dalai_lama_gray.jpg")); //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "Arc-de-Triomphe-France.jpg")); //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "Martin-Schoeller-Jack-Nicholson-Portrait.jpeg")); - ////FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "crop-base-300x200.jpg")); + FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "test2.png")); + + //////FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "crop-base-300x200.jpg")); //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "cmyk.png")); //IEnumerable files = GetFilesByExtensions(di, ".gif"); IEnumerable files = GetFilesByExtensions(di, ".png", ".jpg", ".jpeg"); //IEnumerable files = GetFilesByExtensions(di, ".jpg", ".jpeg", ".jfif"); //IEnumerable files = GetFilesByExtensions(di, ".gif", ".webp", ".bmp", ".jpg", ".png"); - foreach (FileInfo fileInfo in files) - { - if (fileInfo.Name == "test5.jpg") - { - continue; - } + //foreach (FileInfo fileInfo in files) + //{ + // if (fileInfo.Name == "test5.jpg") + // { + // continue; + // } - byte[] photoBytes = File.ReadAllBytes(fileInfo.FullName); - Console.WriteLine("Processing: " + fileInfo.Name); + byte[] photoBytes = File.ReadAllBytes(fileInfo.FullName); + Console.WriteLine("Processing: " + fileInfo.Name); - Stopwatch stopwatch = new Stopwatch(); - stopwatch.Start(); + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); - // ImageProcessor - using (MemoryStream inStream = new MemoryStream(photoBytes)) + // ImageProcessor + using (MemoryStream inStream = new MemoryStream(photoBytes)) + { + using (ImageFactory imageFactory = new ImageFactory(true, false)) { - using (ImageFactory imageFactory = new ImageFactory(true, false)) - { - Size size = new Size(500, 0); - //CropLayer cropLayer = new CropLayer(20, 20, 20, 20, ImageProcessor.Imaging.CropMode.Percentage); - //ResizeLayer layer = new ResizeLayer(size, ResizeMode.Max, AnchorPosition.Center, false); - - //ContentAwareResizeLayer layer = new ContentAwareResizeLayer(size) - //{ - // ConvolutionType = ConvolutionType.Sobel - //}; - // Load, resize, set the format and quality and save an image. - imageFactory.Load(inStream) - //.Overlay(new ImageLayer - // { - // Image = overlay, - // Opacity = 50 - // }) - //.Alpha(50) - //.BackgroundColor(Color.White) - //.Resize(new Size((int)(size.Width * 1.1), 0)) - //.ContentAwareResize(layer) - //.Constrain(size) - //.Mask(mask) - //.Format(new PngFormat()) - //.BackgroundColor(Color.Cyan) - //.ReplaceColor(Color.FromArgb(255, 223, 224), Color.FromArgb(121, 188, 255), 128) - //.Resize(size) - //.Resize(new ResizeLayer(size, ResizeMode.Max)) - // .Resize(new ResizeLayer(size, ResizeMode.Stretch)) - //.DetectEdges(new Laplacian3X3EdgeFilter(), true) - //.DetectEdges(new LaplacianOfGaussianEdgeFilter()) - //.GaussianBlur(new GaussianLayer(10, 11)) - //.EntropyCrop() - .Halftone() - //.RotateBounded(150, false) - //.Crop(cropLayer) - //.Rotate(140) - //.Filter(MatrixFilters.Invert) - //.Contrast(50) - //.Filter(MatrixFilters.Comic) - //.Flip() - //.Filter(MatrixFilters.HiSatch) - //.Pixelate(8) - //.GaussianSharpen(10) - //.Format(new PngFormat() { IsIndexed = true }) - //.Format(new PngFormat() ) - .Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", fileInfo.Name))); - //.Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", Path.GetFileNameWithoutExtension(fileInfo.Name) + ".png"))); - - stopwatch.Stop(); - } + Size size = new Size(500, 0); + //CropLayer cropLayer = new CropLayer(20, 20, 20, 20, ImageProcessor.Imaging.CropMode.Percentage); + //ResizeLayer layer = new ResizeLayer(size, ResizeMode.Max, AnchorPosition.Center, false); + + //ContentAwareResizeLayer layer = new ContentAwareResizeLayer(size) + //{ + // ConvolutionType = ConvolutionType.Sobel + //}; + // Load, resize, set the format and quality and save an image. + imageFactory.Load(inStream) + .DetectObjects(EmbeddedHaarCascades.FrontFaceDefault) + //.Overlay(new ImageLayer + // { + // Image = overlay, + // Opacity = 50 + // }) + //.Alpha(50) + //.BackgroundColor(Color.White) + //.Resize(new Size((int)(size.Width * 1.1), 0)) + //.ContentAwareResize(layer) + //.Constrain(size) + //.Mask(mask) + //.Format(new PngFormat()) + //.BackgroundColor(Color.Cyan) + //.ReplaceColor(Color.FromArgb(255, 223, 224), Color.FromArgb(121, 188, 255), 128) + //.Resize(size) + //.Resize(new ResizeLayer(size, ResizeMode.Max)) + // .Resize(new ResizeLayer(size, ResizeMode.Stretch)) + //.DetectEdges(new Laplacian3X3EdgeFilter(), true) + //.DetectEdges(new LaplacianOfGaussianEdgeFilter()) + //.GaussianBlur(new GaussianLayer(10, 11)) + //.EntropyCrop() + //.Halftone() + //.RotateBounded(150, false) + //.Crop(cropLayer) + //.Rotate(140) + //.Filter(MatrixFilters.Invert) + //.Contrast(50) + //.Filter(MatrixFilters.Comic) + //.Flip() + //.Filter(MatrixFilters.HiSatch) + //.Pixelate(8) + //.GaussianSharpen(10) + //.Format(new PngFormat() { IsIndexed = true }) + //.Format(new PngFormat() ) + .Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", fileInfo.Name))); + //.Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", Path.GetFileNameWithoutExtension(fileInfo.Name) + ".png"))); + + stopwatch.Stop(); } + } - long peakWorkingSet64 = Process.GetCurrentProcess().PeakWorkingSet64; - float mB = peakWorkingSet64 / (float)1024 / 1024; + long peakWorkingSet64 = Process.GetCurrentProcess().PeakWorkingSet64; + float mB = peakWorkingSet64 / (float)1024 / 1024; - Console.WriteLine(@"Completed {0} in {1:s\.fff} secs {2}Peak memory usage was {3} bytes or {4} Mb.", fileInfo.Name, stopwatch.Elapsed, Environment.NewLine, peakWorkingSet64.ToString("#,#"), mB); + Console.WriteLine(@"Completed {0} in {1:s\.fff} secs {2}Peak memory usage was {3} bytes or {4} Mb.", fileInfo.Name, stopwatch.Elapsed, Environment.NewLine, peakWorkingSet64.ToString("#,#"), mB); - //Console.WriteLine("Processed: " + fileInfo.Name + " in " + stopwatch.ElapsedMilliseconds + "ms"); - } + //Console.WriteLine("Processed: " + fileInfo.Name + " in " + stopwatch.ElapsedMilliseconds + "ms"); + //} Console.ReadLine(); } diff --git a/src/ImageProcessor.Playground/images/input/haarcascade_frontalface_alt.xml.REMOVED.git-id b/src/ImageProcessor.Playground/images/input/haarcascade_frontalface_alt.xml.REMOVED.git-id new file mode 100644 index 000000000..7b391478a --- /dev/null +++ b/src/ImageProcessor.Playground/images/input/haarcascade_frontalface_alt.xml.REMOVED.git-id @@ -0,0 +1 @@ +b3860ad6afc1b4eeab715c0de52156a24ec976e7 \ No newline at end of file diff --git a/src/ImageProcessor.Playground/images/input/test2.png.REMOVED.git-id b/src/ImageProcessor.Playground/images/input/test2.png.REMOVED.git-id new file mode 100644 index 000000000..a2d067191 --- /dev/null +++ b/src/ImageProcessor.Playground/images/input/test2.png.REMOVED.git-id @@ -0,0 +1 @@ +64bcd395a5b08c75a48cc21db2668cfb7fe5364e \ No newline at end of file diff --git a/src/ImageProcessor/Common/Extensions/AssemblyExtensions.cs b/src/ImageProcessor/Common/Extensions/AssemblyExtensions.cs index c5c0883e8..1f94afc53 100644 --- a/src/ImageProcessor/Common/Extensions/AssemblyExtensions.cs +++ b/src/ImageProcessor/Common/Extensions/AssemblyExtensions.cs @@ -15,6 +15,7 @@ namespace ImageProcessor.Common.Extensions using System.IO; using System.Linq; using System.Reflection; + using System.Text; /// /// Encapsulates a series of time saving extension methods to the class. @@ -48,6 +49,39 @@ namespace ImageProcessor.Common.Extensions } } + /// + /// Converts an assembly resource into a string. + /// + /// + /// The to load the strings from. + /// + /// + /// The resource. + /// + /// + /// The character encoding to return the resource in. + /// + /// + /// The . + /// + public static string GetResourceAsString(this Assembly assembly, string resource, Encoding encoding = null) + { + encoding = encoding ?? Encoding.UTF8; + + using (MemoryStream ms = new MemoryStream()) + { + using (Stream manifestResourceStream = assembly.GetManifestResourceStream(resource)) + { + if (manifestResourceStream != null) + { + manifestResourceStream.CopyTo(ms); + } + } + + return encoding.GetString(ms.GetBuffer()).Replace('\0', ' ').Trim(); + } + } + /// /// Returns the identifying the file used to load the assembly /// diff --git a/src/ImageProcessor/Common/Extensions/RectangleExtensions.cs b/src/ImageProcessor/Common/Extensions/RectangleExtensions.cs new file mode 100644 index 000000000..a2e831026 --- /dev/null +++ b/src/ImageProcessor/Common/Extensions/RectangleExtensions.cs @@ -0,0 +1,19 @@ +namespace ImageProcessor.Common.Extensions +{ + using System; + using System.Drawing; + + internal static class RectangleExtensions + { + /// + /// Compares two rectangles for equality, considering an acceptance threshold. + /// + public static bool IsEqual(this Rectangle objA, Rectangle objB, int threshold) + { + return (Math.Abs(objA.X - objB.X) < threshold) && + (Math.Abs(objA.Y - objB.Y) < threshold) && + (Math.Abs(objA.Width - objB.Width) < threshold) && + (Math.Abs(objA.Height - objB.Height) < threshold); + } + } +} diff --git a/src/ImageProcessor/ImageFactory.cs b/src/ImageProcessor/ImageFactory.cs index a2dce12c9..c671d7255 100644 --- a/src/ImageProcessor/ImageFactory.cs +++ b/src/ImageProcessor/ImageFactory.cs @@ -21,6 +21,7 @@ namespace ImageProcessor using ImageProcessor.Common.Exceptions; using ImageProcessor.Imaging; using ImageProcessor.Imaging.Filters.EdgeDetection; + using ImageProcessor.Imaging.Filters.ObjectDetection; using ImageProcessor.Imaging.Filters.Photo; using ImageProcessor.Imaging.Formats; using ImageProcessor.Imaging.Helpers; @@ -497,6 +498,17 @@ namespace ImageProcessor return this; } + public ImageFactory DetectObjects(HaarCascade cascade, bool drawRectangles = true, Color color = default(Color)) + { + if (this.ShouldProcess) + { + DetectObjects detectObjects = new DetectObjects { DynamicParameter = cascade }; + this.CurrentImageFormat.ApplyProcessor(detectObjects.ProcessImage, this); + } + + return this; + } + /// /// Crops an image to the area of greatest entropy. /// diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj index 773ac2ba2..958bcc7f4 100644 --- a/src/ImageProcessor/ImageProcessor.csproj +++ b/src/ImageProcessor/ImageProcessor.csproj @@ -126,6 +126,7 @@ + @@ -161,6 +162,21 @@ Code + + + + + + + + + + + + + + + @@ -220,6 +236,7 @@ + @@ -257,6 +274,12 @@ + + + + + + \ No newline at end of file diff --git a/src/ImageProcessor/ImageProcessor.csproj.DotSettings b/src/ImageProcessor/ImageProcessor.csproj.DotSettings new file mode 100644 index 000000000..bf68fcc20 --- /dev/null +++ b/src/ImageProcessor/ImageProcessor.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/FastBitmap.cs b/src/ImageProcessor/Imaging/FastBitmap.cs index f8b14deb2..2bed9cf1e 100644 --- a/src/ImageProcessor/Imaging/FastBitmap.cs +++ b/src/ImageProcessor/Imaging/FastBitmap.cs @@ -13,6 +13,7 @@ namespace ImageProcessor.Imaging using System; using System.Drawing; using System.Drawing.Imaging; + using System.Runtime.InteropServices; using ImageProcessor.Imaging.Colors; @@ -46,6 +47,34 @@ namespace ImageProcessor.Imaging /// private int color32Size; + /// + /// The color channel - blue, green, red, alpha. + /// + private int channel; + + /// + /// Whether to compute tilted integral rectangles. + /// + private bool computeTilted; + + private long[,] nSumImage; // normal integral image + private long[,] sSumImage; // squared integral image + private long[,] tSumImage; // tilted integral image + + private long* nSum; // normal integral image + private long* sSum; // squared integral image + private long* tSum; // tilted integral image + + private GCHandle nSumHandle; + private GCHandle sSumHandle; + private GCHandle tSumHandle; + + private int nWidth; + private int nHeight; + + private int tWidth; + private int tHeight; + /// /// The bitmap data. /// @@ -74,11 +103,51 @@ namespace ImageProcessor.Imaging /// /// The input bitmap. public FastBitmap(Image bitmap) + : this(bitmap, 2, false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The input bitmap. + /// + /// The integral color channel. Blue, Green, Red, or Alpha. + /// + /// + /// Whether to compute tilted integral rectangles. + /// + public FastBitmap(Image bitmap, int integralColorChannel, bool computeTilted) { this.bitmap = (Bitmap)bitmap; this.width = this.bitmap.Width; this.height = this.bitmap.Height; + this.channel = integralColorChannel; + this.computeTilted = computeTilted; + + this.nWidth = this.width + 1; + this.nHeight = this.height + 1; + + this.tWidth = this.width + 2; + this.tHeight = this.height + 2; + + this.nSumImage = new long[this.nHeight, this.nWidth]; + this.nSumHandle = GCHandle.Alloc(this.nSumImage, GCHandleType.Pinned); + this.nSum = (long*)this.nSumHandle.AddrOfPinnedObject().ToPointer(); + + this.sSumImage = new long[this.nHeight, this.nWidth]; + this.sSumHandle = GCHandle.Alloc(this.sSumImage, GCHandleType.Pinned); + this.sSum = (long*)this.sSumHandle.AddrOfPinnedObject().ToPointer(); + + if (this.computeTilted) + { + this.tSumImage = new long[this.tHeight, this.tWidth]; + this.tSumHandle = GCHandle.Alloc(this.tSumImage, GCHandleType.Pinned); + this.tSum = (long*)this.tSumHandle.AddrOfPinnedObject().ToPointer(); + } + this.LockBitmap(); + this.CalculateIntegrals(); } /// @@ -202,6 +271,69 @@ namespace ImageProcessor.Imaging data->A = color.A; } + /// + /// Gets the sum of the pixels in a rectangle of the Integral image. + /// + /// The horizontal position of the rectangle x. + /// The vertical position of the rectangle y. + /// The rectangle's width w. + /// The rectangle's height h. + /// + /// The sum of all pixels contained in the rectangle, computed + /// as I[y, x] + I[y + h, x + w] - I[y + h, x] - I[y, x + w]. + /// + public long GetSum(int x, int y, int rectangleWidth, int rectangleHeight) + { + int a = (this.nWidth * y) + x; + int b = (this.nWidth * (y + rectangleHeight)) + (x + rectangleWidth); + int c = (this.nWidth * (y + rectangleHeight)) + x; + int d = (this.nWidth * y) + (x + rectangleWidth); + + return this.nSum[a] + this.nSum[b] - this.nSum[c] - this.nSum[d]; + } + + /// + /// Gets the sum of the squared pixels in a rectangle of the Integral image. + /// + /// The horizontal position of the rectangle x. + /// The vertical position of the rectangle y. + /// The rectangle's width w. + /// The rectangle's height h. + /// + /// The sum of all pixels contained in the rectangle, computed + /// as I²[y, x] + I²[y + h, x + w] - I²[y + h, x] - I²[y, x + w]. + /// + public long GetSum2(int x, int y, int rectangleWidth, int rectangleHeight) + { + int a = (this.nWidth * y) + x; + int b = (this.nWidth * (y + rectangleHeight)) + (x + rectangleWidth); + int c = (this.nWidth * (y + rectangleHeight)) + x; + int d = (this.nWidth * y) + (x + rectangleWidth); + + return this.sSum[a] + this.sSum[b] - this.sSum[c] - this.sSum[d]; + } + + /// + /// Gets the sum of the pixels in a tilted rectangle of the Integral image. + /// + /// The horizontal position of the rectangle x. + /// The vertical position of the rectangle y. + /// The rectangle's width w. + /// The rectangle's height h. + /// + /// The sum of all pixels contained in the rectangle, computed + /// as T[y + w, x + w + 1] + T[y + h, x - h + 1] - T[y, x + 1] - T[y + w + h, x + w - h + 1]. + /// + public long GetSumT(int x, int y, int rectangleWidth, int rectangleHeight) + { + int a = (this.tWidth * (y + rectangleWidth)) + (x + rectangleWidth + 1); + int b = (this.tWidth * (y + rectangleHeight)) + (x - rectangleHeight + 1); + int c = (this.tWidth * y) + (x + 1); + int d = (this.tWidth * (y + rectangleWidth + rectangleHeight)) + (x + rectangleWidth - rectangleHeight + 1); + + return this.tSum[a] + this.tSum[b] - this.tSum[c] - this.tSum[d]; + } + /// /// Disposes the object and frees resources for the Garbage Collector. /// @@ -266,6 +398,24 @@ namespace ImageProcessor.Imaging // Call the appropriate methods to clean up // unmanaged resources here. + if (this.nSumHandle.IsAllocated) + { + this.nSumHandle.Free(); + this.nSum = null; + } + + if (this.sSumHandle.IsAllocated) + { + this.sSumHandle.Free(); + this.sSum = null; + } + + if (this.tSumHandle.IsAllocated) + { + this.tSumHandle.Free(); + this.tSum = null; + } + // Note disposing is done. this.isDisposed = true; } @@ -294,6 +444,111 @@ namespace ImageProcessor.Imaging this.pixelBase = (byte*)this.bitmapData.Scan0.ToPointer(); } + /// + /// Computes all possible rectangular areas in the image. + /// + private void CalculateIntegrals() + { + // Calculate integral and integral squared values. + int stride = this.bitmapData.Stride; + int offset = stride - this.bytesInARow; + + byte* srcStart = this.pixelBase + this.channel; + + // Do the job + byte* src = srcStart; + + // For each line + // TODO. Make this parallel + for (int y = 1; y <= this.height; y++) + { + int yy = this.nWidth * y; + int y1 = this.nWidth * (y - 1); + + // For each pixel + for (int x = 1; x <= this.width; x++, src += this.color32Size) + { + int pixel = *src; + int pixelSquared = pixel * pixel; + + int r = yy + x; + int a = yy + (x - 1); + int b = y1 + x; + int c = y1 + (x - 1); + + this.nSum[r] = pixel + this.nSum[a] + this.nSum[b] - this.nSum[c]; + this.sSum[r] = pixelSquared + this.sSum[a] + this.sSum[b] - this.sSum[c]; + } + + src += offset; + } + + if (this.computeTilted) + { + src = srcStart; + + // Left-to-right, top-to-bottom pass + for (int y = 1; y <= this.height; y++, src += offset) + { + int yy = this.tWidth * y; + int y1 = this.tWidth * (y - 1); + + for (int x = 2; x < this.width + 2; x++, src += this.color32Size) + { + int a = y1 + (x - 1); + int b = yy + (x - 1); + int c = y1 + (x - 2); + int r = yy + x; + + this.tSum[r] = *src + this.tSum[a] + this.tSum[b] - this.tSum[c]; + } + } + + { + int yy = this.tWidth * this.height; + int y1 = this.tWidth * (this.height + 1); + + for (int x = 2; x < this.width + 2; x++, src += this.color32Size) + { + int a = yy + (x - 1); + int c = yy + (x - 2); + int b = y1 + (x - 1); + int r = y1 + x; + + this.tSum[r] = this.tSum[a] + this.tSum[b] - this.tSum[c]; + } + } + + // Right-to-left, bottom-to-top pass + for (int y = this.height; y >= 0; y--) + { + int yy = this.tWidth * y; + int y1 = this.tWidth * (y + 1); + + for (int x = this.width + 1; x >= 1; x--) + { + int r = yy + x; + int b = y1 + (x - 1); + + this.tSum[r] += this.tSum[b]; + } + } + + for (int y = this.height + 1; y >= 0; y--) + { + int yy = this.tWidth * y; + + for (int x = this.width + 1; x >= 2; x--) + { + int r = yy + x; + int b = yy + (x - 2); + + this.tSum[r] -= this.tSum[b]; + } + } + } + } + /// /// Unlocks the bitmap from system memory. /// diff --git a/src/ImageProcessor/Imaging/Filters/Binarization/BinaryThreshold.cs b/src/ImageProcessor/Imaging/Filters/Binarization/BinaryThreshold.cs index e0f1178b7..41eb3530c 100644 --- a/src/ImageProcessor/Imaging/Filters/Binarization/BinaryThreshold.cs +++ b/src/ImageProcessor/Imaging/Filters/Binarization/BinaryThreshold.cs @@ -1,9 +1,13 @@ // -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) James South. -// // Licensed under the Apache License, Version 2.0. +// Licensed under the Apache License, Version 2.0. // +// +// Performs binary threshold filtering against a given greyscale image. +// // -------------------------------------------------------------------------------------------------------------------- + namespace ImageProcessor.Imaging.Filters.Binarization { using System.Drawing; diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/Class1.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/Class1.cs new file mode 100644 index 000000000..6597c73c8 --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/Class1.cs @@ -0,0 +1,338 @@ +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + + /// + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] + [System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)] + public partial class opencv_storage + { + + private opencv_storageCascade cascadeField; + + /// + public opencv_storageCascade cascade + { + get + { + return this.cascadeField; + } + set + { + this.cascadeField = value; + } + } + } + + /// + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] + public partial class opencv_storageCascade + { + + private string stageTypeField; + + private string featureTypeField; + + private byte heightField; + + private byte widthField; + + private opencv_storageCascadeStageParams stageParamsField; + + private opencv_storageCascadeFeatureParams featureParamsField; + + private byte stageNumField; + + private opencv_storageCascade_[] stagesField; + + private opencv_storageCascade_2[] featuresField; + + private string type_idField; + + /// + public string stageType + { + get + { + return this.stageTypeField; + } + set + { + this.stageTypeField = value; + } + } + + /// + public string featureType + { + get + { + return this.featureTypeField; + } + set + { + this.featureTypeField = value; + } + } + + /// + public byte height + { + get + { + return this.heightField; + } + set + { + this.heightField = value; + } + } + + /// + public byte width + { + get + { + return this.widthField; + } + set + { + this.widthField = value; + } + } + + /// + public opencv_storageCascadeStageParams stageParams + { + get + { + return this.stageParamsField; + } + set + { + this.stageParamsField = value; + } + } + + /// + public opencv_storageCascadeFeatureParams featureParams + { + get + { + return this.featureParamsField; + } + set + { + this.featureParamsField = value; + } + } + + /// + public byte stageNum + { + get + { + return this.stageNumField; + } + set + { + this.stageNumField = value; + } + } + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("_", IsNullable = false)] + public opencv_storageCascade_[] stages + { + get + { + return this.stagesField; + } + set + { + this.stagesField = value; + } + } + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("_", IsNullable = false)] + public opencv_storageCascade_2[] features + { + get + { + return this.featuresField; + } + set + { + this.featuresField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string type_id + { + get + { + return this.type_idField; + } + set + { + this.type_idField = value; + } + } + } + + /// + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] + public partial class opencv_storageCascadeStageParams + { + + private byte maxWeakCountField; + + /// + public byte maxWeakCount + { + get + { + return this.maxWeakCountField; + } + set + { + this.maxWeakCountField = value; + } + } + } + + /// + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] + public partial class opencv_storageCascadeFeatureParams + { + + private byte maxCatCountField; + + /// + public byte maxCatCount + { + get + { + return this.maxCatCountField; + } + set + { + this.maxCatCountField = value; + } + } + } + + /// + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] + public partial class opencv_storageCascade_ + { + + private byte maxWeakCountField; + + private double stageThresholdField; + + private opencv_storageCascade__[] weakClassifiersField; + + /// + public byte maxWeakCount + { + get + { + return this.maxWeakCountField; + } + set + { + this.maxWeakCountField = value; + } + } + + /// + public double stageThreshold + { + get + { + return this.stageThresholdField; + } + set + { + this.stageThresholdField = value; + } + } + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("_", IsNullable = false)] + public opencv_storageCascade__[] weakClassifiers + { + get + { + return this.weakClassifiersField; + } + set + { + this.weakClassifiersField = value; + } + } + } + + /// + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] + public partial class opencv_storageCascade__ + { + + private string internalNodesField; + + private string leafValuesField; + + /// + public string internalNodes + { + get + { + return this.internalNodesField; + } + set + { + this.internalNodesField = value; + } + } + + /// + public string leafValues + { + get + { + return this.leafValuesField; + } + set + { + this.leafValuesField = value; + } + } + } + + /// + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] + public partial class opencv_storageCascade_2 + { + + private string[] rectsField; + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("_", IsNullable = false)] + public string[] rects + { + get + { + return this.rectsField; + } + set + { + this.rectsField = value; + } + } + } + + +} diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/EmbeddedHaarCascades.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/EmbeddedHaarCascades.cs new file mode 100644 index 000000000..d59a6417d --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/EmbeddedHaarCascades.cs @@ -0,0 +1,33 @@ +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System.IO; + using System.Reflection; + + using ImageProcessor.Common.Extensions; + + public static class EmbeddedHaarCascades + { + private static HaarCascade frontFaceDefault; + + public static HaarCascade FrontFaceDefault + { + get + { + return frontFaceDefault ?? (frontFaceDefault = GetCascadeFromResource("haarcascade_frontalface_legacy.xml")); + } + } + + private static HaarCascade GetCascadeFromResource(string identifier) + { + HaarCascade cascade; + var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("ImageProcessor.Imaging.Filters.ObjectDetection.Resources." + identifier); + + //using (StringReader stringReader = new StringReader(resource)) + //{ + cascade = HaarCascade.FromXml(resource); + //} + + return cascade; + } + } +} diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/GroupMatching.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/GroupMatching.cs new file mode 100644 index 000000000..9cb79cc3c --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/GroupMatching.cs @@ -0,0 +1,239 @@ +// Accord Vision Library +// The Accord.NET Framework +// http://accord-framework.net +// +// Copyright © César Souza, 2009-2015 +// cesarsouza at gmail.com +// +// This code has been submitted as an user contribution by darko.juric2 +// GCode Issue #12 https://code.google.com/p/accord/issues/detail?id=12 +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +// + +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System; + using System.Collections.Generic; + + /// + /// Group matching algorithm for detection region averaging. + /// + /// + /// + /// This class can be seen as a post-processing filter. Its goal is to + /// group near or contained regions together in order to produce more + /// robust and smooth estimates of the detected regions. + /// + /// + public abstract class GroupMatching + { + + private int classCount; + private int minNeighbors; + + private int[] labels; + private int[] equals; + + private List filter; + + /// + /// Creates a new object. + /// + /// + /// + /// The minimum number of neighbors needed to keep a detection. If a rectangle + /// has less than this minimum number, it will be discarded as a false positive. + /// + protected GroupMatching(int minimumNeighbors = 2) + { + this.minNeighbors = minimumNeighbors; + this.filter = new List(); + } + + /// + /// Gets or sets the minimum number of neighbors necessary to keep a detection. + /// If a rectangle has less neighbors than this number, it will be discarded as + /// a false positive. + /// + /// + public int MinimumNeighbors + { + get { return minNeighbors; } + set + { + if (minNeighbors < 0) + throw new ArgumentOutOfRangeException("value", "Value must be equal to or higher than zero."); + minNeighbors = value; + } + } + + /// + /// Gets how many classes were found in the + /// last call to . + /// + /// + public int Classes + { + get { return classCount; } + } + + /// + /// Groups possibly near rectangles into a smaller + /// set of distinct and averaged rectangles. + /// + /// + /// The rectangles to group. + /// + public T[] Group(T[] shapes) + { + // Start by classifying rectangles according to distance + classify(shapes); // assign label for near rectangles + + int[] neighborCount; + + // Average the rectangles contained in each labeled group + T[] output = Average(labels, shapes, out neighborCount); + + // Check suppression + if (minNeighbors > 0) + { + filter.Clear(); + + // Discard weak rectangles which don't have enough neighbors + for (int i = 0; i < output.Length; i++) + if (neighborCount[i] >= minNeighbors) filter.Add(output[i]); + + return filter.ToArray(); + } + + return output; + } + + + + /// + /// Detects rectangles which are near and + /// assigns similar class labels accordingly. + /// + /// + private void classify(T[] shapes) + { + equals = new int[shapes.Length]; + for (int i = 0; i < equals.Length; i++) + equals[i] = -1; + + labels = new int[shapes.Length]; + for (int i = 0; i < labels.Length; i++) + labels[i] = i; + + classCount = 0; + + // If two rectangles are near, or contained in + // each other, merge then in a single rectangle + for (int i = 0; i < shapes.Length - 1; i++) + { + for (int j = i + 1; j < shapes.Length; j++) + { + if (Near(shapes[i], shapes[j])) + merge(labels[i], labels[j]); + } + } + + // Count the number of classes and centroids + int[] centroids = new int[shapes.Length]; + for (int i = 0; i < centroids.Length; i++) + if (equals[i] == -1) centroids[i] = classCount++; + + // Classify all rectangles with their labels + for (int i = 0; i < shapes.Length; i++) + { + int root = labels[i]; + while (equals[root] != -1) + root = equals[root]; + + labels[i] = centroids[root]; + } + } + + /// + /// Merges two labels. + /// + /// + private void merge(int label1, int label2) + { + int root1 = label1; + int root2 = label2; + + // Get the roots associated with the two labels + while (equals[root1] != -1) root1 = equals[root1]; + while (equals[root2] != -1) root2 = equals[root2]; + + if (root1 == root2) // labels are already connected + return; + + int minRoot, maxRoot; + int labelWithMinRoot, labelWithMaxRoot; + + if (root1 > root2) + { + maxRoot = root1; + minRoot = root2; + + labelWithMaxRoot = label1; + labelWithMinRoot = label2; + } + else + { + maxRoot = root2; + minRoot = root1; + + labelWithMaxRoot = label2; + labelWithMinRoot = label1; + } + + equals[maxRoot] = minRoot; + + for (int root = maxRoot + 1; root <= labelWithMaxRoot; root++) + { + if (equals[root] == maxRoot) + equals[root] = minRoot; + } + } + + /// + /// When overridden in a child class, should compute + /// whether two given shapes are near. Definition of + /// near is up to the implementation. + /// + /// + /// True if the two shapes are near; false otherwise. + /// + protected abstract bool Near(T shape1, T shape2); + + /// + /// When overridden in a child class, should compute + /// an average of the shapes given as parameters. + /// + /// + /// The label of each shape. + /// The shapes themselves. + /// Should return how many neighbors each shape had. + /// + /// The averaged shapes found in the given parameters. + /// + protected abstract T[] Average(int[] labels, T[] shapes, out int[] neighborCounts); + } +} \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascade.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascade.cs new file mode 100644 index 000000000..1d0ff06df --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascade.cs @@ -0,0 +1,243 @@ +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System; + using System.Globalization; + using System.IO; + using System.Xml; + using System.Xml.Serialization; + + /// + /// Cascade of Haar-like features' weak classification stages. + /// + /// + /// + /// + /// The Viola-Jones object detection framework is the first object detection framework + /// to provide competitive object detection rates in real-time proposed in 2001 by Paul + /// Viola and Michael Jones. Although it can be trained to detect a variety of object + /// classes, it was motivated primarily by the problem of face detection. + /// + /// + /// The implementation of this code has used Viola and Jones' original publication, the + /// OpenCV Library and the Marilena Project as reference. OpenCV is released under a BSD + /// license, it is free for both academic and commercial use. Please be aware that some + /// particular versions of the Haar object detection framework are patented by Viola and + /// Jones and may be subject to restrictions for use in commercial applications. + /// + /// + /// References: + /// + /// + /// + /// Viola, P. and Jones, M. (2001). Rapid Object Detection using a Boosted Cascade + /// of Simple Features. + /// + /// + /// Wikipedia, The Free Encyclopedia. Viola-Jones object detection framework + /// + /// + /// + /// + /// + /// + /// To load an OpenCV-compatible XML definition for a Haar cascade, you can use HaarCascade's + /// FromXml static method. An example would be: + /// + /// String path = @"C:\Users\haarcascade-frontalface_alt2.xml"; + /// HaarCascade cascade1 = HaarCascade.FromXml(path); + /// + /// + /// + /// After the cascade has been loaded, it is possible to create a new + /// using the cascade. Please see for more details. It is also + /// possible to generate embeddable C# definitions from a cascade, avoiding the need to load + /// XML files on program startup. Please see method or + /// class for details. + /// + /// + [Serializable] + public class HaarCascade : ICloneable + { + /// + /// Gets the stages' base width. + /// + /// + public int Width { get; protected set; } + + /// + /// Gets the stages' base height. + /// + /// + public int Height { get; protected set; } + + /// + /// Gets the classification stages. + /// + /// + public HaarCascadeStage[] Stages { get; protected set; } + + /// + /// Gets a value indicating whether this cascade has tilted features. + /// + /// + /// + /// true if this cascade has tilted features; otherwise, false. + /// + /// + public bool HasTiltedFeatures { get; protected set; } + + /// + /// Constructs a new Haar Cascade. + /// + /// + /// Base feature width. + /// Base feature height. + /// Haar-like features classification stages. + /// + public HaarCascade(int baseWidth, int baseHeight, HaarCascadeStage[] stages) + { + Width = baseWidth; + Height = baseHeight; + Stages = stages; + + // check if the classifier has tilted features + HasTiltedFeatures = checkTiltedFeatures(stages); + } + + /// + /// Constructs a new Haar Cascade. + /// + /// + /// Base feature width. + /// Base feature height. + /// + protected HaarCascade(int baseWidth, int baseHeight) + { + Width = baseWidth; + Height = baseHeight; + } + + + /// + /// Checks if the classifier contains tilted (rotated) features + /// + /// + private static bool checkTiltedFeatures(HaarCascadeStage[] stages) + { + foreach (var stage in stages) + foreach (var tree in stage.Trees) + foreach (var node in tree) + if (node.Feature.Tilted == true) + return true; + return false; + } + + /// + /// Creates a new object that is a copy of the current instance. + /// + /// + /// + /// A new object that is a copy of this instance. + /// + /// + public object Clone() + { + HaarCascadeStage[] newStages = new HaarCascadeStage[Stages.Length]; + for (int i = 0; i < newStages.Length; i++) + newStages[i] = (HaarCascadeStage)Stages[i].Clone(); + + HaarCascade r = new HaarCascade(Width, Height); + r.HasTiltedFeatures = this.HasTiltedFeatures; + r.Stages = newStages; + + return r; + } + + + /// + /// Loads a HaarCascade from a OpenCV-compatible XML file. + /// + /// + /// + /// A containing the file stream + /// for the xml definition of the classifier to be loaded. + /// + /// The HaarCascadeClassifier loaded from the file. + /// + public static HaarCascade FromXml(Stream stream) + { + return FromXml(new StreamReader(stream)); + } + + /// + /// Loads a HaarCascade from a OpenCV-compatible XML file. + /// + /// + /// + /// The file path for the xml definition of the classifier to be loaded. + /// + /// The HaarCascadeClassifier loaded from the file. + /// + public static HaarCascade FromXml(string path) + { + return FromXml(new StreamReader(path)); + } + + /// + /// Loads a HaarCascade from a OpenCV-compatible XML file. + /// + /// + /// + /// A containing the file stream + /// for the xml definition of the classifier to be loaded. + /// + /// The HaarCascadeClassifier loaded from the file. + /// + public static HaarCascade FromXml(TextReader stringReader) + { + XmlTextReader xmlReader = new XmlTextReader(stringReader); + + // Gathers the base window size + xmlReader.ReadToFollowing("size"); + string size = xmlReader.ReadElementContentAsString(); + //xmlReader.ReadToFollowing("height"); + //int baseHeight = int.Parse(xmlReader.ReadElementContentAsString().Trim(), CultureInfo.InvariantCulture); + + //xmlReader.ReadToFollowing("width"); + //int baseWidth = int.Parse(xmlReader.ReadElementContentAsString().Trim(), CultureInfo.InvariantCulture); + + + // Proceeds to load the cascade stages + xmlReader.ReadToFollowing("stages"); + XmlSerializer serializer = new XmlSerializer(typeof(HaarCascadeSerializationObject)); + var stages = (HaarCascadeSerializationObject)serializer.Deserialize(xmlReader); + + // Process base window size + string[] s = size.Trim().Split(' '); + int baseWidth = int.Parse(s[0], CultureInfo.InvariantCulture); + int baseHeight = int.Parse(s[1], CultureInfo.InvariantCulture); + + // Create and return the new cascade + return new HaarCascade(baseWidth, baseHeight, stages.Stages); + } + + /// + /// Saves a HaarCascade to C# code. + /// + /// + public void ToCode(string path, string className) + { + ToCode(new StreamWriter(path), className); + } + + /// + /// Saves a HaarCascade to C# code. + /// + /// + public void ToCode(TextWriter textWriter, string className) + { + new HaarCascadeWriter(textWriter).Write(this, className); + } + + } +} diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascadeSerializationObject.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascadeSerializationObject.cs new file mode 100644 index 000000000..765bae6bd --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascadeSerializationObject.cs @@ -0,0 +1,21 @@ +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System; + using System.Xml.Serialization; + + /// + /// Haar Cascade Serialization Root. This class is used + /// only for XML serialization/deserialization. + /// + /// + [Serializable] + [XmlRoot(Namespace = "", IsNullable = false, ElementName = "stages")] + public class HaarCascadeSerializationObject + { + /// + /// The stages retrieved after deserialization. + /// + [XmlElement("_")] + public HaarCascadeStage[] Stages { get; set; } + } +} \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascadeStage.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascadeStage.cs new file mode 100644 index 000000000..a9d51dbbc --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascadeStage.cs @@ -0,0 +1,168 @@ +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System; + using System.Xml.Serialization; + + /// + /// Haar Cascade Classifier Stage. + /// + /// + /// + /// A Haar Cascade Classifier is composed of several stages. Each stage + /// contains a set of classifier trees used in the decision process. + /// + /// + [Serializable] + [XmlRoot("_")] + public class HaarCascadeStage : ICloneable + { + /// + /// Gets or sets the feature trees and its respective + /// feature tree nodes which compose this stage. + /// + /// + [XmlArray("trees")] + [XmlArrayItem("_")] + [XmlArrayItem("_", NestingLevel = 1)] + public HaarFeatureNode[][] Trees { get; set; } + + /// + /// Gets or sets the threshold associated with this stage, + /// i.e. the minimum value the classifiers should output + /// to decide if the image contains the object or not. + /// + /// + [XmlElement("stage_threshold")] + public double Threshold { get; set; } + + /// + /// Gets the index of the parent stage from this stage. + /// + /// + [XmlElement("parent")] + public int ParentIndex { get; set; } + + /// + /// Gets the index of the next stage from this stage. + /// + /// + [XmlElement("next")] + public int NextIndex { get; set; } + + /// + /// Constructs a new Haar Cascade Stage. + /// + /// + public HaarCascadeStage() + { + } + + /// + /// Constructs a new Haar Cascade Stage. + /// + /// + public HaarCascadeStage(double threshold) + { + this.Threshold = threshold; + } + + /// + /// Constructs a new Haar Cascade Stage. + /// + /// + public HaarCascadeStage(double threshold, int parentIndex, int nextIndex) + { + this.Threshold = threshold; + this.ParentIndex = parentIndex; + this.NextIndex = nextIndex; + } + + /// + /// Classifies an image as having the searched object or not. + /// + /// + public bool Classify(FastBitmap image, int x, int y, double factor) + { + double value = 0; + + // For each feature in the feature tree of the current stage, + foreach (HaarFeatureNode[] tree in Trees) + { + int current = 0; + + do + { + // Get the feature node from the tree + HaarFeatureNode node = tree[current]; + + // Evaluate the node's feature + double sum = node.Feature.GetSum(image, x, y); + + // And increase the value accumulator + if (sum < node.Threshold * factor) + { + value += node.LeftValue; + current = node.LeftNodeIndex; + } + else + { + value += node.RightValue; + current = node.RightNodeIndex; + } + + } while (current > 0); + + // Stop early if we have already surpassed the stage threshold value. + //if (value > this.Threshold) return true; + } + + // After we have evaluated the output for the + // current stage, we will check if the value + // is still lesser than the stage threshold. + if (value < this.Threshold) + { + // If it is, the stage has rejected the current + // image and it doesn't contains our object. + return false; + } + else + { + // The stage has accepted the current image + return true; + } + } + + + /// + /// Creates a new object that is a copy of the current instance. + /// + /// + /// + /// A new object that is a copy of this instance. + /// + /// + public object Clone() + { + HaarFeatureNode[][] newTrees = new HaarFeatureNode[Trees.Length][]; + + for (int i = 0; i < newTrees.Length; i++) + { + HaarFeatureNode[] tree = Trees[i]; + HaarFeatureNode[] newTree = newTrees[i] = + new HaarFeatureNode[tree.Length]; + + for (int j = 0; j < newTree.Length; j++) + newTree[j] = (HaarFeatureNode)tree[j].Clone(); + } + + HaarCascadeStage r = new HaarCascadeStage(); + r.NextIndex = NextIndex; + r.ParentIndex = ParentIndex; + r.Threshold = Threshold; + r.Trees = newTrees; + + return r; + } + + } +} diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascadeWriter.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascadeWriter.cs new file mode 100644 index 000000000..ef3f76bac --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarCascadeWriter.cs @@ -0,0 +1,154 @@ +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System; + using System.Globalization; + using System.IO; + + /// + /// Automatic transcriber for Haar cascades. + /// + /// + /// + /// This class can be used to generate code-only definitions for Haar cascades, + /// avoiding the need for loading and parsing XML files during application startup. + /// This class generates C# code for a class inheriting from + /// which may be used to create a . + /// + /// + public class HaarCascadeWriter + { + private TextWriter writer; + + /// + /// Constructs a new class. + /// + /// The stream to write to. + /// + public HaarCascadeWriter(TextWriter stream) + { + this.writer = stream; + } + + /// + /// Writes the specified cascade. + /// + /// The cascade to write. + /// The name for the generated class. + /// + public void Write(HaarCascade cascade, string className) + { + for (int i = 0; i < cascade.Stages.Length; i++) + for (int j = 0; j < cascade.Stages[i].Trees.Length; j++) + if (cascade.Stages[i].Trees[j].Length != 1) + throw new ArgumentException("Only cascades with single node trees are currently supported."); + + + writer.WriteLine("// This file has been automatically transcribed by the"); + writer.WriteLine("//"); + writer.WriteLine("// Accord Vision Library"); + writer.WriteLine("// The Accord.NET Framework"); + writer.WriteLine("// http://accord-framework.net"); + writer.WriteLine("//"); + writer.WriteLine(); + writer.WriteLine("namespace HaarCascades"); + writer.WriteLine("{"); + writer.WriteLine(" using System.CodeDom.Compiler;"); + writer.WriteLine(" using System.Collections.Generic;"); + writer.WriteLine(); + writer.WriteLine(" /// "); + writer.WriteLine(" /// Automatically generated haar-cascade definition"); + writer.WriteLine(" /// to use with the Accord.NET Framework object detectors."); + writer.WriteLine(" /// "); + writer.WriteLine(" /// "); + writer.WriteLine(" [GeneratedCode(\"Accord.NET HaarCascadeWriter\", \"2.7\")]"); + writer.WriteLine(" public class {0} : Accord.Vision.Detection.HaarCascade", className); + writer.WriteLine(" {"); + writer.WriteLine(); + writer.WriteLine(" /// "); + writer.WriteLine(" /// Automatically generated transcription"); + writer.WriteLine(" /// "); + writer.WriteLine(" public {0}()", className); + writer.WriteLine(" : base({0}, {1})", cascade.Width, cascade.Height); + writer.WriteLine(" {"); + writer.WriteLine(" List stages = new List();"); + writer.WriteLine(" List nodes;"); + writer.WriteLine(" HaarCascadeStage stage;"); + writer.WriteLine(); + + if (cascade.HasTiltedFeatures) + { + writer.WriteLine(" HasTiltedFeatures = true;"); + writer.WriteLine(); + } + + // Write cascade stages + for (int i = 0; i < cascade.Stages.Length; i++) + writeStage(i, cascade.Stages[i]); + + writer.WriteLine(); + writer.WriteLine(" Stages = stages.ToArray();"); + writer.WriteLine(" }"); + writer.WriteLine(" }"); + writer.WriteLine("}"); + } + + private void writeStage(int i, HaarCascadeStage stage) + { + writer.WriteLine(" #region Stage {0}", i); + writer.WriteLine(" stage = new HaarCascadeStage({0}, {1}, {2}); nodes = new List();", + stage.Threshold.ToString("R", NumberFormatInfo.InvariantInfo), + stage.ParentIndex, stage.NextIndex); + + // Write stage trees + for (int j = 0; j < stage.Trees.Length; j++) + writeTrees(stage, j); + + writer.WriteLine(" stage.Trees = nodes.ToArray(); stages.Add(stage);"); + writer.WriteLine(" #endregion"); + writer.WriteLine(); + } + + private void writeTrees(HaarCascadeStage stage, int j) + { + writer.Write(" nodes.Add(new[] { "); + + // Assume trees have single node + writeFeature(stage.Trees[j][0]); + + writer.WriteLine(" });"); + } + + private void writeFeature(HaarFeatureNode node) + { + + writer.Write("new HaarFeatureNode({0}, {1}, {2}, ", + node.Threshold.ToString("R", NumberFormatInfo.InvariantInfo), + node.LeftValue.ToString("R", NumberFormatInfo.InvariantInfo), + node.RightValue.ToString("R", NumberFormatInfo.InvariantInfo)); + + if (node.Feature.Tilted) + writer.Write("true, "); + + // Write Haar-like rectangular features + for (int k = 0; k < node.Feature.Rectangles.Length; k++) + { + writeRectangle(node.Feature.Rectangles[k]); + + if (k < node.Feature.Rectangles.Length - 1) + writer.Write(", "); + } + + writer.Write(" )"); + } + + private void writeRectangle(HaarRectangle rectangle) + { + writer.Write("new int[] {{ {0}, {1}, {2}, {3}, {4} }}", + rectangle.X.ToString(NumberFormatInfo.InvariantInfo), + rectangle.Y.ToString(NumberFormatInfo.InvariantInfo), + rectangle.Width.ToString(NumberFormatInfo.InvariantInfo), + rectangle.Height.ToString(NumberFormatInfo.InvariantInfo), + rectangle.Weight.ToString("R", NumberFormatInfo.InvariantInfo)); + } + } +} diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarClassifier.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarClassifier.cs new file mode 100644 index 000000000..e5e3b056a --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarClassifier.cs @@ -0,0 +1,175 @@ +// Accord Vision Library +// The Accord.NET Framework (LGPL) +// http://accord-framework.net +// +// Copyright © César Souza, 2009-2015 +// cesarsouza at gmail.com +// +// Copyright © Masakazu Ohtsuka, 2008 +// This work is partially based on the original Project Marilena code, +// distributed under a 2-clause BSD License. Details are listed below. +// +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistribution's of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistribution's in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// This software is provided by the copyright holders and contributors "as is" and +// any express or implied warranties, including, but not limited to, the implied +// warranties of merchantability and fitness for a particular purpose are disclaimed. +// In no event shall the Intel Corporation or contributors be liable for any direct, +// indirect, incidental, special, exemplary, or consequential damages +// (including, but not limited to, procurement of substitute goods or services; +// loss of use, data, or profits; or business interruption) however caused +// and on any theory of liability, whether in contract, strict liability, +// or tort (including negligence or otherwise) arising in any way out of +// the use of this software, even if advised of the possibility of such damage. +// + +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System; + using System.Drawing; + + /// + /// Strong classifier based on a weaker cascade of + /// classifiers using Haar-like rectangular features. + /// + /// + /// + /// + /// The Viola-Jones object detection framework is the first object detection framework + /// to provide competitive object detection rates in real-time proposed in 2001 by Paul + /// Viola and Michael Jones. Although it can be trained to detect a variety of object + /// classes, it was motivated primarily by the problem of face detection. + /// + /// + /// The implementation of this code has used Viola and Jones' original publication, the + /// OpenCV Library and the Marilena Project as reference. OpenCV is released under a BSD + /// license, it is free for both academic and commercial use. Please be aware that some + /// particular versions of the Haar object detection framework are patented by Viola and + /// Jones and may be subject to restrictions for use in commercial applications. The code + /// has been implemented with full support for tilted Haar features. + /// + /// + /// References: + /// + /// + /// + /// Viola, P. and Jones, M. (2001). Rapid Object Detection using a Boosted Cascade + /// of Simple Features. + /// + /// + /// http://en.wikipedia.org/wiki/Viola-Jones_object_detection_framework + /// + /// + /// + /// + /// + [Serializable] + public class HaarClassifier + { + private HaarCascade cascade; + + private float invArea; + private float scale; + + + /// + /// Constructs a new classifier. + /// + /// + public HaarClassifier(HaarCascade cascade) + { + this.cascade = cascade; + } + + /// + /// Constructs a new classifier. + /// + /// + public HaarClassifier(int baseWidth, int baseHeight, HaarCascadeStage[] stages) + : this(new HaarCascade(baseWidth, baseHeight, stages)) { } + + + /// + /// Gets the cascade of weak-classifiers + /// used by this strong classifier. + /// + /// + public HaarCascade Cascade + { + get { return cascade; } + } + + /// + /// Gets or sets the scale of the search window + /// being currently used by the classifier. + /// + /// + public float Scale + { + get { return this.scale; } + set + { + if (this.scale == value) + return; + + this.scale = value; + this.invArea = 1f / (cascade.Width * cascade.Height * scale * scale); + + // For each stage in the cascade + foreach (HaarCascadeStage stage in cascade.Stages) + { + // For each tree in the cascade + foreach (HaarFeatureNode[] tree in stage.Trees) + { + // For each feature node in the tree + foreach (HaarFeatureNode node in tree) + { + // Set the scale and weight for the node feature + node.Feature.SetScaleAndWeight(value, invArea); + } + } + } + } + } + + + /// + /// Detects the presence of an object in a given window. + /// + public bool Compute(FastBitmap image, Rectangle rectangle) + { + int x = rectangle.X; + int y = rectangle.Y; + int w = rectangle.Width; + int h = rectangle.Height; + + double mean = image.GetSum(x, y, w, h) * invArea; + double var = image.GetSum2(x, y, w, h) * invArea - (mean * mean); + + double sdev = (var >= 0) ? Math.Sqrt(var) : 1; + + // For each classification stage in the cascade + foreach (HaarCascadeStage stage in cascade.Stages) + { + // Check if the stage has rejected the image + if (stage.Classify(image, x, y, sdev) == false) + { + return false; // The image has been rejected. + } + } + + // If the object has gone all stages and has not + // been rejected, the object has been detected. + return true; // The image has been detected. + } + } +} \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarFeature.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarFeature.cs new file mode 100644 index 000000000..09e33b0cf --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarFeature.cs @@ -0,0 +1,223 @@ +// Accord Vision Library +// The Accord.NET Framework (LGPL) +// http://accord-framework.net +// +// Copyright © César Souza, 2009-2015 +// cesarsouza at gmail.com +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +// + +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System; + using System.Collections.Generic; + using System.Xml; + using System.Xml.Schema; + using System.Xml.Serialization; + + /// + /// Rectangular Haar-like feature container. + /// + /// + /// References: + /// - http://en.wikipedia.org/wiki/Haar-like_features#Rectangular_Haar-like_features + /// + [Serializable] + public sealed class HaarFeature : IXmlSerializable, ICloneable + { + /// + /// Gets or sets whether this feature is tilted. + /// + /// + public bool Tilted { get; set; } + + /// + /// Gets or sets the Haar rectangles for this feature. + /// + /// + public HaarRectangle[] Rectangles { get; set; } + + + /// + /// Constructs a new Haar-like feature. + /// + /// + public HaarFeature() + { + this.Rectangles = new HaarRectangle[2]; + } + + /// + /// Constructs a new Haar-like feature. + /// + /// + public HaarFeature(params HaarRectangle[] rectangles) + { + this.Rectangles = rectangles; + } + + /// + /// Constructs a new Haar-like feature. + /// + /// + public HaarFeature(params int[][] rectangles) + : this(false, rectangles) { } + + /// + /// Constructs a new Haar-like feature. + /// + /// + public HaarFeature(bool tilted, params int[][] rectangles) + { + this.Tilted = tilted; + this.Rectangles = new HaarRectangle[rectangles.Length]; + for (int i = 0; i < rectangles.Length; i++) + this.Rectangles[i] = new HaarRectangle(rectangles[i]); + } + + /// + /// Gets the sum of the areas of the rectangular features in an integral image. + /// + /// The containing integral rectangle information. + /// The x coordinate. + /// The y coordinate. + /// + /// The representing the sum. + /// + public double GetSum(FastBitmap image, int x, int y) + { + double sum = 0.0; + + if (!Tilted) + { + // Compute the sum for a standard feature + foreach (HaarRectangle rect in Rectangles) + { + sum += image.GetSum(x + rect.ScaledX, y + rect.ScaledY, + rect.ScaledWidth, rect.ScaledHeight) * rect.ScaledWeight; + } + } + else + { + // Compute the sum for a rotated feature + foreach (HaarRectangle rect in Rectangles) + { + sum += image.GetSumT(x + rect.ScaledX, y + rect.ScaledY, + rect.ScaledWidth, rect.ScaledHeight) * rect.ScaledWeight; + } + } + + return sum; + } + + /// + /// Sets the scale and weight of a Haar-like rectangular feature container. + /// + /// + public void SetScaleAndWeight(float scale, float weight) + { + // Manual loop unfolding + if (Rectangles.Length == 2) + { + HaarRectangle a = Rectangles[0]; + HaarRectangle b = Rectangles[1]; + + b.ScaleRectangle(scale); + b.ScaleWeight(weight); + + a.ScaleRectangle(scale); + a.ScaledWeight = -(b.Area * b.ScaledWeight) / a.Area; + } + else // rectangles.Length == 3 + { + HaarRectangle a = Rectangles[0]; + HaarRectangle b = Rectangles[1]; + HaarRectangle c = Rectangles[2]; + + c.ScaleRectangle(scale); + c.ScaleWeight(weight); + + b.ScaleRectangle(scale); + b.ScaleWeight(weight); + + a.ScaleRectangle(scale); + a.ScaledWeight = -(b.Area * b.ScaledWeight + + c.Area * c.ScaledWeight) / (a.Area); + } + } + + + #region IXmlSerializable Members + + XmlSchema IXmlSerializable.GetSchema() + { + throw new NotSupportedException(); + } + + void IXmlSerializable.ReadXml(XmlReader reader) + { + reader.ReadStartElement("feature"); + + reader.ReadToFollowing("rects"); + reader.ReadToFollowing("_"); + + var rec = new List(); + while (reader.Name == "_") + { + string str = reader.ReadElementContentAsString(); + rec.Add(HaarRectangle.Parse(str)); + + while (reader.Name != "_" && reader.Name != "tilted" && + reader.NodeType != XmlNodeType.EndElement) + reader.Read(); + } + + Rectangles = rec.ToArray(); + + reader.ReadToFollowing("tilted", reader.BaseURI); + Tilted = reader.ReadElementContentAsInt() == 1; + + reader.ReadEndElement(); + } + + void IXmlSerializable.WriteXml(XmlWriter writer) + { + throw new NotSupportedException(); + } + + #endregion + + + + /// + /// Creates a new object that is a copy of the current instance. + /// + /// + /// A new object that is a copy of this instance. + /// + public object Clone() + { + HaarRectangle[] newRectangles = new HaarRectangle[this.Rectangles.Length]; + for (int i = 0; i < newRectangles.Length; i++) + { + HaarRectangle rect = Rectangles[i]; + newRectangles[i] = new HaarRectangle(rect.X, rect.Y, rect.Width, rect.Height, rect.Weight); + } + + return new HaarFeature { Rectangles = newRectangles, Tilted = this.Tilted }; + } + } +} \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarFeatureNode.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarFeatureNode.cs new file mode 100644 index 000000000..e81004f72 --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarFeatureNode.cs @@ -0,0 +1,122 @@ +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System; + using System.Xml.Serialization; + + /// + /// Haar Cascade Feature Tree Node. + /// + /// + /// + /// The Feature Node is a node belonging to a feature tree, + /// containing information about child nodes and an associated + /// . + /// + /// + [Serializable] + public class HaarFeatureNode : ICloneable + { + private int rightNodeIndex = -1; + private int leftNodeIndex = -1; + + /// + /// Gets the threshold for this feature. + /// + /// + [XmlElement("threshold")] + public double Threshold { get; set; } + + /// + /// Gets the left value for this feature. + /// + /// + [XmlElement("left_val")] + public double LeftValue { get; set; } + + /// + /// Gets the right value for this feature. + /// + /// + [XmlElement("right_val")] + public double RightValue { get; set; } + + /// + /// Gets the left node index for this feature. + /// + /// + [XmlElement("left_node")] + public int LeftNodeIndex + { + get { return leftNodeIndex; } + set { leftNodeIndex = value; } + } + + /// + /// Gets the right node index for this feature. + /// + /// + [XmlElement("right_node")] + public int RightNodeIndex + { + get { return rightNodeIndex; } + set { rightNodeIndex = value; } + } + + /// + /// Gets the feature associated with this node. + /// + /// + [XmlElement("feature", IsNullable = false)] + public HaarFeature Feature { get; set; } + + /// + /// Constructs a new feature tree node. + /// + public HaarFeatureNode() + { + } + + /// + /// Constructs a new feature tree node. + /// + /// + public HaarFeatureNode(double threshold, double leftValue, double rightValue, params int[][] rectangles) + : this(threshold, leftValue, rightValue, false, rectangles) { } + + /// + /// Constructs a new feature tree node. + /// + /// + public HaarFeatureNode(double threshold, double leftValue, double rightValue, bool tilted, params int[][] rectangles) + { + this.Feature = new HaarFeature(tilted, rectangles); + this.Threshold = threshold; + this.LeftValue = leftValue; + this.RightValue = rightValue; + } + + + /// + /// Creates a new object that is a copy of the current instance. + /// + /// + /// A new object that is a copy of this instance. + /// + /// + public object Clone() + { + HaarFeatureNode r = new HaarFeatureNode(); + + r.Feature = (HaarFeature)Feature.Clone(); + r.Threshold = Threshold; + + r.RightValue = RightValue; + r.LeftValue = LeftValue; + + r.LeftNodeIndex = leftNodeIndex; + r.RightNodeIndex = rightNodeIndex; + + return r; + } + } +} diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarRectangle.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarRectangle.cs new file mode 100644 index 000000000..dd4889884 --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarCascade/HaarRectangle.cs @@ -0,0 +1,203 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Represents a rectangle which can be at any position and scale within the original image. +// Based on original code found in the Accord Framework at +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System; + using System.Globalization; + + /// + /// Represents a rectangle which can be at any position and scale within the original image. + /// Based on original code found in the Accord Framework at + /// + [Serializable] + public class HaarRectangle : ICloneable + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The values for this rectangle. + /// + public HaarRectangle(int[] values) + { + this.X = values[0]; + this.Y = values[1]; + this.Width = values[2]; + this.Height = values[3]; + this.Weight = values[4]; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The x coordinate marking the top-left point to apply to this rectangle. + /// + /// + /// The y coordinate marking the top-left point to apply to this rectangle. + /// + /// + /// The width to apply to this rectangle. + /// + /// + /// The height to apply to this rectangle. + /// + /// + /// The weight to apply to this rectangle. + /// + public HaarRectangle(int x, int y, int width, int height, float weight) + { + this.X = x; + this.Y = y; + this.Width = width; + this.Height = height; + this.Weight = weight; + } + + /// + /// Prevents a default instance of the class from being created. + /// + private HaarRectangle() + { + } + + /// + /// Gets or sets the x-coordinate of this Haar feature rectangle. + /// + public int X { get; set; } + + /// + /// Gets or sets the y-coordinate of this Haar feature rectangle. + /// + public int Y { get; set; } + + /// + /// Gets or sets the width of this Haar feature rectangle. + /// + public int Width { get; set; } + + /// + /// Gets or sets the height of this Haar feature rectangle. + /// + public int Height { get; set; } + + /// + /// Gets or sets the weight of this Haar feature rectangle. + /// + public float Weight { get; set; } + + /// + /// Gets or sets the scaled x-coordinate of this Haar feature rectangle. + /// + public int ScaledX { get; set; } + + /// + /// Gets or sets the scaled y-coordinate of this Haar feature rectangle. + /// + public int ScaledY { get; set; } + + /// + /// Gets or sets the scaled width of this Haar feature rectangle. + /// + public int ScaledWidth { get; set; } + + /// + /// Gets or sets the scaled height of this Haar feature rectangle. + /// + public int ScaledHeight { get; set; } + + /// + /// Gets or sets the scaled weight of this Haar feature rectangle. + /// + public float ScaledWeight { get; set; } + + /// + /// Gets the area of this rectangle. + /// + public int Area + { + get { return this.ScaledWidth * this.ScaledHeight; } + } + + /// + /// Converts a from a string representation. + /// + /// + /// The value to parse. + /// + /// + /// The . + /// + public static HaarRectangle Parse(string value) + { + string[] values = value.Trim().Split(' '); + + int x = int.Parse(values[0], CultureInfo.InvariantCulture); + int y = int.Parse(values[1], CultureInfo.InvariantCulture); + int w = int.Parse(values[2], CultureInfo.InvariantCulture); + int h = int.Parse(values[3], CultureInfo.InvariantCulture); + float weight = float.Parse(values[4], CultureInfo.InvariantCulture); + + return new HaarRectangle(x, y, w, h, weight); + } + + /// + /// Scales the values of this rectangle. + /// + /// + /// The scale. + /// + public void ScaleRectangle(float scale) + { + this.ScaledX = (int)(this.X * scale); + this.ScaledY = (int)(this.Y * scale); + this.ScaledWidth = (int)(this.Width * scale); + this.ScaledHeight = (int)(this.Height * scale); + } + + /// + /// Scales the weight of this rectangle. + /// + /// + /// The scale. + /// + public void ScaleWeight(float scale) + { + this.ScaledWeight = this.Weight * scale; + } + + /// + /// Creates a new object that is a copy of the current instance. + /// + /// + /// A new object that is a copy of this instance. + /// + public object Clone() + { + HaarRectangle r = new HaarRectangle + { + Height = this.Height, + ScaledHeight = this.ScaledHeight, + ScaledWeight = this.ScaledWeight, + ScaledWidth = this.ScaledWidth, + ScaledX = this.ScaledX, + ScaledY = this.ScaledY, + Weight = this.Weight, + Width = this.Width, + X = this.X, + Y = this.Y + }; + + return r; + } + } +} diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarObjectDetector.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarObjectDetector.cs new file mode 100644 index 000000000..100f5c686 --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/HaarObjectDetector.cs @@ -0,0 +1,594 @@ +// Accord Vision Library +// The Accord.NET Framework (LGPL) +// http://accord-framework.net +// +// Copyright © César Souza, 2009-2015 +// cesarsouza at gmail.com +// +// Copyright © Masakazu Ohtsuka, 2008 +// This work is partially based on the original Project Marilena code, +// distributed under a 2-clause BSD License. Details are listed below. +// +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistribution's of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * Redistribution's in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// This software is provided by the copyright holders and contributors "as is" and +// any express or implied warranties, including, but not limited to, the implied +// warranties of merchantability and fitness for a particular purpose are disclaimed. +// In no event shall the Intel Corporation or contributors be liable for any direct, +// indirect, incidental, special, exemplary, or consequential damages +// (including, but not limited to, procurement of substitute goods or services; +// loss of use, data, or profits; or business interruption) however caused +// and on any theory of liability, whether in contract, strict liability, +// or tort (including negligence or otherwise) arising in any way out of +// the use of this software, even if advised of the possibility of such damage. +// + +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System; + using System.Collections.Generic; + using System.Drawing; + using System.Drawing.Imaging; + using System.Threading.Tasks; + + using ImageProcessor.Common.Extensions; + using ImageProcessor.Imaging.Colors; + + /// + /// Viola-Jones Object Detector based on Haar-like features. + /// + /// + /// + /// + /// The Viola-Jones object detection framework is the first object detection framework + /// to provide competitive object detection rates in real-time proposed in 2001 by Paul + /// Viola and Michael Jones. Although it can be trained to detect a variety of object + /// classes, it was motivated primarily by the problem of face detection. + /// + /// + /// The implementation of this code has used Viola and Jones' original publication, the + /// OpenCV Library and the Marilena Project as reference. OpenCV is released under a BSD + /// license, it is free for both academic and commercial use. Please be aware that some + /// particular versions of the Haar object detection framework are patented by Viola and + /// Jones and may be subject to restrictions for use in commercial applications. The code + /// has been implemented with full support for tilted Haar features from the ground up. + /// + /// + /// References: + /// + /// + /// + /// Viola, P. and Jones, M. (2001). Rapid Object Detection using a Boosted Cascade + /// of Simple Features. + /// + /// + /// http://en.wikipedia.org/wiki/Viola-Jones_object_detection_framework + /// + /// + /// + /// + /// + public class HaarObjectDetector + { + + private List detectedObjects; + private HaarClassifier classifier; + + private ObjectDetectorSearchMode searchMode = ObjectDetectorSearchMode.NoOverlap; + private ObjectDetectorScalingMode scalingMode = ObjectDetectorScalingMode.GreaterToSmaller; + + // TODO: Support ROI + // private Rectangle searchWindow; + + private Size minSize = new Size(15, 15); + private Size maxSize = new Size(500, 500); + private float factor = 1.2f; + private int channel = new Color32().R; + + private Rectangle[] lastObjects; + private int steadyThreshold = 2; + + private int baseWidth; + private int baseHeight; + + private int lastWidth; + private int lastHeight; + private float[] steps; + + private RectangleGroupMatching match; + + #region Constructors + + /// + /// Constructs a new Haar object detector. + /// + /// + /// + /// The to use in the detector's classifier. + /// For the default face cascade, please take a look on + /// . + /// + /// + public HaarObjectDetector(HaarCascade cascade) + : this(cascade, 15) { } + + /// + /// Constructs a new Haar object detector. + /// + /// + /// + /// The to use in the detector's classifier. + /// For the default face cascade, please take a look on + /// . + /// + /// Minimum window size to consider when searching for + /// objects. Default value is 15. + /// + public HaarObjectDetector(HaarCascade cascade, int minSize) + : this(cascade, minSize, ObjectDetectorSearchMode.NoOverlap) { } + + /// + /// Constructs a new Haar object detector. + /// + /// + /// + /// The to use in the detector's classifier. + /// For the default face cascade, please take a look on + /// . + /// + /// + /// Minimum window size to consider when searching for + /// objects. Default value is 15. + /// The to use + /// during search. Please see documentation of + /// for details. Default value is + /// + public HaarObjectDetector(HaarCascade cascade, int minSize, ObjectDetectorSearchMode searchMode) + : this(cascade, minSize, searchMode, 1.2f) { } + + /// + /// Constructs a new Haar object detector. + /// + /// + /// + /// The to use in the detector's classifier. + /// For the default face cascade, please take a look on + /// . + /// + /// Minimum window size to consider when searching for + /// objects. Default value is 15. + /// + /// The to use + /// during search. Please see documentation of + /// for details. Default value is + /// The re-scaling factor to use when re-scaling the window during search. + /// + public HaarObjectDetector(HaarCascade cascade, int minSize, + ObjectDetectorSearchMode searchMode, float scaleFactor) + : this(cascade, minSize, searchMode, scaleFactor, ObjectDetectorScalingMode.SmallerToGreater) { } + + /// + /// Constructs a new Haar object detector. + /// + /// + /// + /// The to use in the detector's classifier. + /// For the default face cascade, please take a look on + /// . + /// + /// Minimum window size to consider when searching for + /// objects. Default value is 15. + /// The to use + /// during search. Please see documentation of + /// for details. Default is . + /// The scaling factor to rescale the window + /// during search. Default value is 1.2f. + /// The to use + /// when re-scaling the search window during search. Default is + /// . + /// + public HaarObjectDetector(HaarCascade cascade, int minSize, + ObjectDetectorSearchMode searchMode, float scaleFactor, + ObjectDetectorScalingMode scalingMode) + { + this.classifier = new HaarClassifier(cascade); + this.minSize = new Size(minSize, minSize); + this.searchMode = searchMode; + this.ScalingMode = scalingMode; + this.factor = scaleFactor; + this.detectedObjects = new List(); + + this.baseWidth = cascade.Width; + this.baseHeight = cascade.Height; + + this.match = new RectangleGroupMatching(0, 0.2); + } + #endregion + + #region Properties + /// + /// Minimum window size to consider when searching objects. + /// + /// + public Size MinSize + { + get { return minSize; } + set { minSize = value; } + } + + /// + /// Maximum window size to consider when searching objects. + /// + public Size MaxSize + { + get { return maxSize; } + set { maxSize = value; } + } + + /// + /// Gets or sets the color channel to use when processing color images. + /// + /// + public int Channel + { + get { return channel; } + set { channel = value; } + } + + /// + /// Gets or sets the scaling factor to rescale the window during search. + /// + /// + public float ScalingFactor + { + get { return factor; } + set + { + if (value != factor) + { + factor = value; + steps = null; + } + } + } + + /// + /// Gets or sets the desired searching method. + /// + /// + public ObjectDetectorSearchMode SearchMode + { + get { return searchMode; } + set { searchMode = value; } + } + + /// + /// Gets or sets the desired scaling method. + /// + /// + public ObjectDetectorScalingMode ScalingMode + { + get { return scalingMode; } + set + { + if (value != scalingMode) + { + scalingMode = value; + steps = null; + } + } + } + + /// + /// Gets or sets the minimum threshold used to suppress rectangles which + /// have not been detected sufficient number of times. This property only + /// has effect when is set to . + /// + /// + /// + /// + /// The value of this property represents the minimum amount of detections + /// made inside a region to report this region as an actual detection. For + /// example, setting this property to two will discard all regions which + /// had not achieved at least two detected rectangles within it. + /// + /// + /// Setting this property to a value higher than zero may decrease the + /// number of false positives. + /// + /// + public int Suppression + { + get { return match.MinimumNeighbors; } + set { match.MinimumNeighbors = value; } + } + + /// + /// Gets the detected objects bounding boxes. + /// + /// + public Rectangle[] DetectedObjects + { + get { return detectedObjects.ToArray(); } + } + + /// + /// Gets the internal Cascade Classifier used by this detector. + /// + /// + public HaarClassifier Classifier + { + get { return classifier; } + } + + /// + /// Gets how many frames the object has + /// been detected in a steady position. + /// + /// + /// The number of frames the detected object + /// has been in a steady position. + /// + public int Steady { get; private set; } + + #endregion + + + /// + /// Performs object detection on the given frame. + /// + /// + //public Rectangle[] ProcessFrame(Bitmap frame) + //{ + // using (FastBitmap fastBitmap = new FastBitmap(frame)) + // { + // return ProcessFrame(fastBitmap); + // } + //} + + /// + /// Performs object detection on the given frame. + /// + /// + public Rectangle[] ProcessFrame(Bitmap image) + { + int colorChannel = + image.PixelFormat == PixelFormat.Format8bppIndexed ? 0 : channel; + + Rectangle[] objects; + + // Creates an integral image representation of the frame + using (FastBitmap fastBitmap = new FastBitmap(image, colorChannel, this.classifier.Cascade.HasTiltedFeatures)) + { + // Creates a new list of detected objects. + this.detectedObjects.Clear(); + + int width = fastBitmap.Width; + int height = fastBitmap.Height; + + // Update parameters only if different size + if (steps == null || width != lastWidth || height != lastHeight) + update(width, height); + + + Rectangle window = Rectangle.Empty; + + // For each scaling step + for (int i = 0; i < steps.Length; i++) + { + float scaling = steps[i]; + + // Set the classifier window scale + classifier.Scale = scaling; + + // Get the scaled window size + window.Width = (int)(baseWidth * scaling); + window.Height = (int)(baseHeight * scaling); + + // Check if the window is lesser than the minimum size + if (window.Width < minSize.Width || window.Height < minSize.Height) + { + // If we are searching in greater to smaller mode, + if (scalingMode == ObjectDetectorScalingMode.GreaterToSmaller) + { + break; // it won't get bigger, so we should stop. + } + else continue; // continue until it gets greater. + } + + // Check if the window is greater than the maximum size + else if (window.Width > maxSize.Width || window.Height > maxSize.Height) + { + // If we are searching in greater to smaller mode, + if (scalingMode == ObjectDetectorScalingMode.GreaterToSmaller) + { + continue; // continue until it gets smaller. + } + + break; // it won't get smaller, so we should stop. + } + + // Grab some scan loop parameters + int xStep = window.Width >> 3; + int yStep = window.Height >> 3; + + int xEnd = width - window.Width; + int yEnd = height - window.Height; + + + // Parallel mode. Scan the integral image searching + // for objects in the window with parallelization. + var bag = new System.Collections.Concurrent.ConcurrentBag(); + + int numSteps = (int)Math.Ceiling((double)yEnd / yStep); + + // For each pixel in the window column + var window1 = window; + Parallel.For( + 0, + numSteps, + (j, options) => + { + int y = j * yStep; + + // Create a local window reference + Rectangle localWindow = window1; + + localWindow.Y = y; + + // For each pixel in the window row + for (int x = 0; x < xEnd; x += xStep) + { + if (options.ShouldExitCurrentIteration) return; + + localWindow.X = x; + + // Try to detect and object inside the window + if (classifier.Compute(fastBitmap, localWindow)) + { + // an object has been detected + bag.Add(localWindow); + + if (searchMode == ObjectDetectorSearchMode.Single) + options.Stop(); + } + } + }); + + // If required, avoid adding overlapping objects at + // the expense of extra computation. Otherwise, only + // add objects to the detected objects collection. + if (searchMode == ObjectDetectorSearchMode.NoOverlap) + { + foreach (Rectangle obj in bag) + { + if (!overlaps(obj)) + { + detectedObjects.Add(obj); + } + } + } + else if (searchMode == ObjectDetectorSearchMode.Single) + { + if (bag.TryPeek(out window)) + { + detectedObjects.Add(window); + break; + } + } + else + { + foreach (Rectangle obj in bag) + { + detectedObjects.Add(obj); + } + } + } + } + + objects = detectedObjects.ToArray(); + + if (searchMode == ObjectDetectorSearchMode.Average) + { + objects = match.Group(objects); + } + + checkSteadiness(objects); + lastObjects = objects; + + return objects; // Returns the array of detected objects. + } + + private void update(int width, int height) + { + List listSteps = new List(); + + // Set initial parameters according to scaling mode + if (scalingMode == ObjectDetectorScalingMode.SmallerToGreater) + { + float start = 1f; + float stop = Math.Min(width / (float)baseWidth, height / (float)baseHeight); + float step = factor; + + for (float f = start; f < stop; f *= step) + listSteps.Add(f); + } + else + { + float start = Math.Min(width / (float)baseWidth, height / (float)baseHeight); + float stop = 1f; + float step = 1f / factor; + + for (float f = start; f > stop; f *= step) + listSteps.Add(f); + } + + steps = listSteps.ToArray(); + + lastWidth = width; + lastHeight = height; + } + + private void checkSteadiness(Rectangle[] rectangles) + { + if (lastObjects == null || + rectangles == null || + rectangles.Length == 0) + { + Steady = 0; + return; + } + + bool equals = true; + foreach (Rectangle current in rectangles) + { + bool found = false; + foreach (Rectangle last in lastObjects) + { + if (current.IsEqual(last, steadyThreshold)) + { + found = true; + continue; + } + } + + if (!found) + { + equals = false; + break; + } + } + + if (equals) + { + Steady++; + } + else + { + Steady = 0; + } + } + + private bool overlaps(Rectangle rect) + { + foreach (Rectangle r in detectedObjects) + { + if (rect.IntersectsWith(r)) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/ObjectDetectorScalingMode.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/ObjectDetectorScalingMode.cs new file mode 100644 index 000000000..2899d01fa --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/ObjectDetectorScalingMode.cs @@ -0,0 +1,23 @@ +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + /// + /// Object detector options for window scaling. + /// + /// + public enum ObjectDetectorScalingMode + { + /// + /// Will start with a big search window and + /// gradually scale into smaller ones. + /// + /// + GreaterToSmaller, + + /// + /// Will start with small search windows and + /// gradually scale into greater ones. + /// + /// + SmallerToGreater, + } +} \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/ObjectDetectorSearchMode.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/ObjectDetectorSearchMode.cs new file mode 100644 index 000000000..f20e00eda --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/ObjectDetectorSearchMode.cs @@ -0,0 +1,38 @@ +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + /// + /// Object detector options for the search procedure. + /// + /// + public enum ObjectDetectorSearchMode + { + /// + /// Entire image will be scanned. + /// + /// + Default = 0, + + /// + /// Only a single object will be retrieved. + /// + /// + Single, + + /// + /// If a object has already been detected inside an area, + /// it will not be scanned twice for inner or overlapping + /// objects, saving computation time. + /// + /// + NoOverlap, + + /// + /// If several objects are located within one another, + /// they will be averaged. Additionally, objects which + /// have not been detected sufficient times may be dropped + /// by setting . + /// + /// + Average, + } +} \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/RectangleGroupMatching.cs b/src/ImageProcessor/Imaging/Filters/ObjectDetection/RectangleGroupMatching.cs new file mode 100644 index 000000000..bc2bd1039 --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/RectangleGroupMatching.cs @@ -0,0 +1,126 @@ +// Accord Vision Library +// The Accord.NET Framework +// http://accord-framework.net +// +// Copyright © César Souza, 2009-2015 +// cesarsouza at gmail.com +// +// This code has been submitted as an user contribution by darko.juric2 +// GCode Issue #12 https://code.google.com/p/accord/issues/detail?id=12 +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +// + +namespace ImageProcessor.Imaging.Filters.ObjectDetection +{ + using System; + using System.Drawing; + + /// + /// Group matching algorithm for detection region averaging. + /// + /// + /// + /// This class can be seen as a post-processing filter. Its goal is to + /// group near or contained regions together in order to produce more + /// robust and smooth estimates of the detected regions. + /// + /// + public class RectangleGroupMatching : GroupMatching + { + private double threshold; + + /// + /// Creates a new object. + /// + /// + /// + /// The minimum number of neighbors needed to keep a detection. If a rectangle + /// has less than this minimum number, it will be discarded as a false positive. + /// + /// The minimum distance threshold to consider two rectangles as neighbors. + /// Default is 0.2. + /// + public RectangleGroupMatching(int minimumNeighbors = 2, double threshold = 0.2) + : base(minimumNeighbors) + { + this.threshold = threshold; + } + + /// + /// Gets the minimum distance threshold to consider + /// two rectangles as neighbors. Default is 0.2. + /// + /// + protected double Threshold + { + get { return threshold; } + } + + /// + /// Checks if two rectangles are near. + /// + /// + protected override bool Near(Rectangle shape1, Rectangle shape2) + { + if (shape1.Contains(shape2) || shape2.Contains(shape1)) + return true; + + int minHeight = Math.Min(shape1.Height, shape2.Height); + int minWidth = Math.Min(shape1.Width, shape2.Width); + double delta = 0.5 * threshold * (minHeight + minWidth); + + return Math.Abs(shape1.X - shape2.X) <= delta + && Math.Abs(shape1.Y - shape2.Y) <= delta + && Math.Abs(shape1.Right - shape2.Right) <= delta + && Math.Abs(shape1.Bottom - shape2.Bottom) <= delta; + } + + /// + /// Averages rectangles which belongs to the + /// same class (have the same class label) + /// + /// + protected override Rectangle[] Average(int[] labels, Rectangle[] shapes, out int[] neighborCounts) + { + neighborCounts = new int[Classes]; + + Rectangle[] centroids = new Rectangle[Classes]; + for (int i = 0; i < shapes.Length; i++) + { + int j = labels[i]; + + centroids[j].X += shapes[i].X; + centroids[j].Y += shapes[i].Y; + centroids[j].Width += shapes[i].Width; + centroids[j].Height += shapes[i].Height; + + neighborCounts[j]++; + } + + for (int i = 0; i < centroids.Length; i++) + { + centroids[i] = new Rectangle( + x: (int)Math.Ceiling((float)centroids[i].X / neighborCounts[i]), + y: (int)Math.Ceiling((float)centroids[i].Y / neighborCounts[i]), + width: (int)Math.Ceiling((float)centroids[i].Width / neighborCounts[i]), + height: (int)Math.Ceiling((float)centroids[i].Height / neighborCounts[i])); + } + + return centroids; + } + } + +} \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/Resources/haarcascade_frontalface_alt.xml.REMOVED.git-id b/src/ImageProcessor/Imaging/Filters/ObjectDetection/Resources/haarcascade_frontalface_alt.xml.REMOVED.git-id new file mode 100644 index 000000000..7b391478a --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/Resources/haarcascade_frontalface_alt.xml.REMOVED.git-id @@ -0,0 +1 @@ +b3860ad6afc1b4eeab715c0de52156a24ec976e7 \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/Resources/haarcascade_frontalface_default.xml.REMOVED.git-id b/src/ImageProcessor/Imaging/Filters/ObjectDetection/Resources/haarcascade_frontalface_default.xml.REMOVED.git-id new file mode 100644 index 000000000..71674c521 --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/Resources/haarcascade_frontalface_default.xml.REMOVED.git-id @@ -0,0 +1 @@ +bc2aa3c73e580998987cc543593cff2ffb48aa31 \ No newline at end of file diff --git a/src/ImageProcessor/Imaging/Filters/ObjectDetection/Resources/haarcascade_frontalface_legacy.xml.REMOVED.git-id b/src/ImageProcessor/Imaging/Filters/ObjectDetection/Resources/haarcascade_frontalface_legacy.xml.REMOVED.git-id new file mode 100644 index 000000000..5dc16bacd --- /dev/null +++ b/src/ImageProcessor/Imaging/Filters/ObjectDetection/Resources/haarcascade_frontalface_legacy.xml.REMOVED.git-id @@ -0,0 +1 @@ +eb6792a3e6ca1e3ab14c44ba04153ab71328f027 \ No newline at end of file diff --git a/src/ImageProcessor/Processors/Alpha.cs b/src/ImageProcessor/Processors/Alpha.cs index 9f05b29f7..621f4f8cb 100644 --- a/src/ImageProcessor/Processors/Alpha.cs +++ b/src/ImageProcessor/Processors/Alpha.cs @@ -68,6 +68,7 @@ namespace ImageProcessor.Processors int percentage = this.DynamicParameter; newImage = new Bitmap(image); + newImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); newImage = Adjustments.Alpha(newImage, percentage); image.Dispose(); diff --git a/src/ImageProcessor/Processors/DetectObjects.cs b/src/ImageProcessor/Processors/DetectObjects.cs new file mode 100644 index 000000000..417c55a3a --- /dev/null +++ b/src/ImageProcessor/Processors/DetectObjects.cs @@ -0,0 +1,119 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Encapsulates methods to change the DetectObjects component of the image to effect its transparency. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Processors +{ + using System; + using System.Collections.Generic; + using System.Drawing; + + using ImageProcessor.Common.Exceptions; + using ImageProcessor.Imaging.Filters.ObjectDetection; + using ImageProcessor.Imaging.Filters.Photo; + using ImageProcessor.Imaging.Helpers; + + /// + /// Encapsulates methods to change the DetectObjects component of the image to effect its transparency. + /// + public class DetectObjects : IGraphicsProcessor + { + /// + /// Initializes a new instance of the class. + /// + public DetectObjects() + { + this.Settings = new Dictionary(); + } + + /// + /// Gets or sets the dynamic parameter. + /// + public dynamic DynamicParameter + { + get; + set; + } + + /// + /// Gets or sets any additional settings required by the processor. + /// + public Dictionary Settings + { + get; + set; + } + + /// + /// Processes the image. + /// + /// + /// The current instance of the class containing + /// the image to process. + /// + /// + /// The processed image from the current instance of the class. + /// + public Image ProcessImage(ImageFactory factory) + { + Bitmap newImage = null; + Bitmap grey = null; + Image image = factory.Image; + + try + { + HaarCascade cascade = this.DynamicParameter; + grey = new Bitmap(image.Width, image.Height); + grey.SetResolution(image.HorizontalResolution, image.VerticalResolution); + grey = MatrixFilters.GreyScale.TransformImage(image, grey); + + HaarObjectDetector detector = new HaarObjectDetector(cascade) + { + SearchMode = ObjectDetectorSearchMode.NoOverlap, + ScalingMode = ObjectDetectorScalingMode.GreaterToSmaller, + ScalingFactor = 1.5f + }; + + // Process frame to detect objects + Rectangle[] rectangles = detector.ProcessFrame(grey); + grey.Dispose(); + + newImage = new Bitmap(image); + newImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); + using (Graphics graphics = Graphics.FromImage(newImage)) + { + using (Pen blackPen = new Pen(Color.White)) + { + blackPen.Width = 4; + graphics.DrawRectangles(blackPen, rectangles); + } + } + + image.Dispose(); + image = newImage; + } + catch (Exception ex) + { + if (grey != null) + { + grey.Dispose(); + } + + if (newImage != null) + { + newImage.Dispose(); + } + + throw new ImageProcessingException("Error processing image with " + this.GetType().Name, ex); + } + + return image; + } + } +} diff --git a/src/ImageProcessor/Settings.StyleCop b/src/ImageProcessor/Settings.StyleCop index 094f2cb82..32b31a902 100644 --- a/src/ImageProcessor/Settings.StyleCop +++ b/src/ImageProcessor/Settings.StyleCop @@ -1,11 +1,13 @@ + Accord bitstream dd ddd dllimport gps + Haar Kayyali Laplacian mmmm