// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) James South. // Licensed under the Apache License, Version 2.0. // // // Allows fast access to 's pixel data. // // -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Imaging { using System; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using ImageProcessor.Imaging.Colors; /// /// Allows fast access to 's pixel data. /// public unsafe class FastBitmap : IDisposable { /// /// The bitmap. /// private readonly Bitmap bitmap; /// /// The width of the bitmap. /// private readonly int width; /// /// The height of the bitmap. /// private readonly int height; /// /// The size of the a single pixel. /// private readonly int pixelSize; /// /// The color channel - blue, green, red, alpha. /// private readonly int channel; /// /// Whether to compute tilted integral rectangles. /// private readonly bool computeTilted; /// /// The normal integral image. /// private readonly long[,] normalSumImage; /// /// The squared integral image. /// private readonly long[,] squaredSumImage; /// /// The tilted sum image. /// private readonly long[,] tiltedSumImage; /// /// The normal width. /// private readonly int normalWidth; /// /// The tilted width. /// private readonly int tiltedWidth; /// /// The number of bytes in a row. /// private int bytesInARow; /// /// The normal integral sum. /// private long* normalSum; /// /// The squared integral sum. /// private long* squaredSum; /// /// The tilted integral sum. /// private long* tiltedSum; /// /// The normal sum handle. /// private GCHandle normalSumHandle; /// /// The squared sum handle. /// private GCHandle squaredSumHandle; /// /// The tilted sum handle. /// private GCHandle tiltedSumHandle; /// /// The bitmap data. /// private BitmapData bitmapData; /// /// The position of the first pixel in the bitmap. /// private byte* pixelBase; /// /// A value indicating whether this instance of the given entity has been disposed. /// /// if this instance has been disposed; otherwise, . /// /// If the entity is disposed, it must not be disposed a second /// time. The isDisposed field is set the first time the entity /// is disposed. If the isDisposed field is true, then the Dispose() /// method will not dispose again. This help not to prolong the entity's /// life in the Garbage Collector. /// private bool isDisposed; /// /// Initializes a new instance of the class. /// /// The input bitmap. public FastBitmap(Image bitmap) : this(bitmap, false) { } /// /// Initializes a new instance of the class. /// /// The input bitmap. /// /// Whether to compute tilted integral rectangles. /// public FastBitmap(Image bitmap, bool computeTilted) { // Check image format if (!(bitmap.PixelFormat == PixelFormat.Format8bppIndexed || bitmap.PixelFormat == PixelFormat.Format24bppRgb || bitmap.PixelFormat == PixelFormat.Format32bppArgb || bitmap.PixelFormat == PixelFormat.Format32bppPArgb)) { throw new ArgumentException("Only 8bpp, 24bpp and 32bpp images are supported."); } this.pixelSize = Image.GetPixelFormatSize(bitmap.PixelFormat) / 8; this.bitmap = (Bitmap)bitmap; this.width = this.bitmap.Width; this.height = this.bitmap.Height; this.channel = this.bitmap.PixelFormat == PixelFormat.Format8bppIndexed ? 0 : 2; this.computeTilted = computeTilted; this.normalWidth = this.width + 1; int normalHeight = this.height + 1; this.tiltedWidth = this.width + 2; int tiltedHeight = this.height + 2; this.normalSumImage = new long[normalHeight, this.normalWidth]; this.normalSumHandle = GCHandle.Alloc(this.normalSumImage, GCHandleType.Pinned); this.normalSum = (long*)this.normalSumHandle.AddrOfPinnedObject().ToPointer(); this.squaredSumImage = new long[normalHeight, this.normalWidth]; this.squaredSumHandle = GCHandle.Alloc(this.squaredSumImage, GCHandleType.Pinned); this.squaredSum = (long*)this.squaredSumHandle.AddrOfPinnedObject().ToPointer(); if (this.computeTilted) { this.tiltedSumImage = new long[tiltedHeight, this.tiltedWidth]; this.tiltedSumHandle = GCHandle.Alloc(this.tiltedSumImage, GCHandleType.Pinned); this.tiltedSum = (long*)this.tiltedSumHandle.AddrOfPinnedObject().ToPointer(); } this.LockBitmap(); this.CalculateIntegrals(); } /// /// Gets the width, in pixels of the . /// public int Width { get { return this.width; } } /// /// Gets the height, in pixels of the . /// public int Height { get { return this.height; } } /// /// Gets the Integral Image for values' sum. /// public long[,] NormalImage { get { return this.normalSumImage; } } /// /// Gets the Integral Image for values' squared sum. /// public long[,] SquaredImage { get { return this.squaredSumImage; } } /// /// Gets the Integral Image for tilted values' sum. /// public long[,] TiltedImage { get { return this.tiltedSumImage; } } /// /// Gets the pixel data for the given position. /// /// /// The x position of the pixel. /// /// /// The y position of the pixel. /// /// /// The . /// private Color32* this[int x, int y] { get { return (Color32*)(this.pixelBase + (y * this.bytesInARow) + (x * this.pixelSize)); } } /// /// Allows the implicit conversion of an instance of to a /// . /// /// /// The instance of to convert. /// /// /// An instance of . /// public static implicit operator Image(FastBitmap fastBitmap) { return fastBitmap.bitmap; } /// /// Allows the implicit conversion of an instance of to a /// . /// /// /// The instance of to convert. /// /// /// An instance of . /// public static implicit operator Bitmap(FastBitmap fastBitmap) { return fastBitmap.bitmap; } /// /// Gets the color at the specified pixel of the . /// /// The x-coordinate of the pixel to retrieve. /// The y-coordinate of the pixel to retrieve. /// The at the given pixel. public Color GetPixel(int x, int y) { #if DEBUG if ((x < 0) || (x >= this.width)) { throw new ArgumentOutOfRangeException("x", "Value cannot be less than zero or greater than the bitmap width."); } if ((y < 0) || (y >= this.height)) { throw new ArgumentOutOfRangeException("y", "Value cannot be less than zero or greater than the bitmap height."); } #endif Color32* data = this[x, y]; if (this.bitmap.PixelFormat == PixelFormat.Format32bppArgb || this.bitmap.PixelFormat == PixelFormat.Format32bppPArgb) { return Color.FromArgb(data->A, data->R, data->G, data->B); } return Color.FromArgb(data->R, data->G, data->B); } /// /// Sets the color of the specified pixel of the . /// /// The x-coordinate of the pixel to set. /// The y-coordinate of the pixel to set. /// /// A color structure that represents the /// color to set the specified pixel. /// public void SetPixel(int x, int y, Color color) { #if DEBUG if ((x < 0) || (x >= this.width)) { throw new ArgumentOutOfRangeException("x", "Value cannot be less than zero or greater than the bitmap width."); } if ((y < 0) || (y >= this.height)) { throw new ArgumentOutOfRangeException("y", "Value cannot be less than zero or greater than the bitmap height."); } #endif Color32* data = this[x, y]; data->R = color.R; data->G = color.G; data->B = color.B; if (this.bitmap.PixelFormat == PixelFormat.Format32bppArgb || this.bitmap.PixelFormat == PixelFormat.Format32bppPArgb) { 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.normalWidth * y) + x; int b = (this.normalWidth * (y + rectangleHeight)) + (x + rectangleWidth); int c = (this.normalWidth * (y + rectangleHeight)) + x; int d = (this.normalWidth * y) + (x + rectangleWidth); return this.normalSum[a] + this.normalSum[b] - this.normalSum[c] - this.normalSum[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.normalWidth * y) + x; int b = (this.normalWidth * (y + rectangleHeight)) + (x + rectangleWidth); int c = (this.normalWidth * (y + rectangleHeight)) + x; int d = (this.normalWidth * y) + (x + rectangleWidth); return this.squaredSum[a] + this.squaredSum[b] - this.squaredSum[c] - this.squaredSum[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.tiltedWidth * (y + rectangleWidth)) + (x + rectangleWidth + 1); int b = (this.tiltedWidth * (y + rectangleHeight)) + (x - rectangleHeight + 1); int c = (this.tiltedWidth * y) + (x + 1); int d = (this.tiltedWidth * (y + rectangleWidth + rectangleHeight)) + (x + rectangleWidth - rectangleHeight + 1); return this.tiltedSum[a] + this.tiltedSum[b] - this.tiltedSum[c] - this.tiltedSum[d]; } /// /// Disposes the object and frees resources for the Garbage Collector. /// public void Dispose() { this.Dispose(true); // This object will be cleaned up by the Dispose method. // Therefore, you should call GC.SuppressFinalize to // take this object off the finalization queue // and prevent finalization code for this object // from executing a second time. GC.SuppressFinalize(this); } /// /// Determines whether the specified is equal to the current . /// /// /// true if the specified object is equal to the current object; otherwise, false. /// /// The object to compare with the current object. public override bool Equals(object obj) { FastBitmap fastBitmap = obj as FastBitmap; if (fastBitmap == null) { return false; } return this.bitmap == fastBitmap.bitmap; } /// /// Serves as a hash function for a particular type. /// /// /// A hash code for the current . /// public override int GetHashCode() { return this.bitmap.GetHashCode(); } /// /// Disposes the object and frees resources for the Garbage Collector. /// /// If true, the object gets disposed. protected virtual void Dispose(bool disposing) { if (this.isDisposed) { return; } if (disposing) { // Dispose of any managed resources here. this.UnlockBitmap(); } // Call the appropriate methods to clean up // unmanaged resources here. if (this.normalSumHandle.IsAllocated) { this.normalSumHandle.Free(); this.normalSum = null; } if (this.squaredSumHandle.IsAllocated) { this.squaredSumHandle.Free(); this.squaredSum = null; } if (this.tiltedSumHandle.IsAllocated) { this.tiltedSumHandle.Free(); this.tiltedSum = null; } // Note disposing is done. this.isDisposed = true; } /// /// Locks the bitmap into system memory. /// private void LockBitmap() { Rectangle bounds = new Rectangle(Point.Empty, this.bitmap.Size); // Figure out the number of bytes in a row. This is rounded up to be a multiple // of 4 bytes, since a scan line in an image must always be a multiple of 4 bytes // in length. this.bytesInARow = bounds.Width * this.pixelSize; if (this.bytesInARow % 4 != 0) { this.bytesInARow = 4 * ((this.bytesInARow / 4) + 1); } // Lock the bitmap this.bitmapData = this.bitmap.LockBits(bounds, ImageLockMode.ReadWrite, this.bitmap.PixelFormat); // Set the value to the first scan line 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 for (int y = 1; y <= this.height; y++) { int yy = this.normalWidth * y; int y1 = this.normalWidth * (y - 1); // For each pixel for (int x = 1; x <= this.width; x++, src += this.pixelSize) { int pixel = *src; int pixelSquared = pixel * pixel; int r = yy + x; int a = yy + (x - 1); int b = y1 + x; int g = y1 + (x - 1); this.normalSum[r] = pixel + this.normalSum[a] + this.normalSum[b] - this.normalSum[g]; this.squaredSum[r] = pixelSquared + this.squaredSum[a] + this.squaredSum[b] - this.squaredSum[g]; } 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.tiltedWidth * y; int y1 = this.tiltedWidth * (y - 1); for (int x = 2; x < this.width + 2; x++, src += this.pixelSize) { int a = y1 + (x - 1); int b = yy + (x - 1); int g = y1 + (x - 2); int r = yy + x; this.tiltedSum[r] = *src + this.tiltedSum[a] + this.tiltedSum[b] - this.tiltedSum[g]; } } { int yy = this.tiltedWidth * this.height; int y1 = this.tiltedWidth * (this.height + 1); for (int x = 2; x < this.width + 2; x++, src += this.pixelSize) { int a = yy + (x - 1); int c = yy + (x - 2); int b = y1 + (x - 1); int r = y1 + x; this.tiltedSum[r] = this.tiltedSum[a] + this.tiltedSum[b] - this.tiltedSum[c]; } } // Right-to-left, bottom-to-top pass for (int y = this.height; y >= 0; y--) { int yy = this.tiltedWidth * y; int y1 = this.tiltedWidth * (y + 1); for (int x = this.width + 1; x >= 1; x--) { int r = yy + x; int b = y1 + (x - 1); this.tiltedSum[r] += this.tiltedSum[b]; } } for (int y = this.height + 1; y >= 0; y--) { int yy = this.tiltedWidth * y; for (int x = this.width + 1; x >= 2; x--) { int r = yy + x; int b = yy + (x - 2); this.tiltedSum[r] -= this.tiltedSum[b]; } } } } /// /// Unlocks the bitmap from system memory. /// private void UnlockBitmap() { // Copy the RGB values back to the bitmap and unlock the bitmap. this.bitmap.UnlockBits(this.bitmapData); this.bitmapData = null; this.pixelBase = null; } } }