mirror of https://github.com/SixLabors/ImageSharp
Browse Source
Replace quantizer and added wait to semaphore slim. TODO: Replace method call in ImageFactory methods. Former-commit-id: 4c3fe3c01d8a3d857fcc16e8add7fa93fe4df602af/merge-core
9 changed files with 940 additions and 881 deletions
@ -1,836 +0,0 @@ |
|||||
// --------------------------------------------------------------------------------------------------------------------
|
|
||||
// <copyright file="ColorQuantizer.cs" company="James South">
|
|
||||
// Copyright (c) James South.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
// </copyright>
|
|
||||
// <summary>
|
|
||||
// The color quantizer.
|
|
||||
// </summary>
|
|
||||
// --------------------------------------------------------------------------------------------------------------------
|
|
||||
|
|
||||
namespace ImageProcessor.Imaging |
|
||||
{ |
|
||||
#region Using
|
|
||||
using System; |
|
||||
using System.Diagnostics.CodeAnalysis; |
|
||||
using System.Drawing; |
|
||||
using System.Drawing.Imaging; |
|
||||
using System.Runtime.InteropServices; |
|
||||
#endregion
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// The color quantizer.
|
|
||||
/// </summary>
|
|
||||
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")] |
|
||||
public static class ColorQuantizer |
|
||||
{ |
|
||||
#region Quantize methods
|
|
||||
/// <summary>The quantize.</summary>
|
|
||||
/// <param name="image">The image.</param>
|
|
||||
/// <param name="bitmapPixelFormat">The bitmap pixel format.</param>
|
|
||||
/// <returns>The quantized image with the recalculated color palette.</returns>
|
|
||||
public static Bitmap Quantize(Image image, PixelFormat bitmapPixelFormat) |
|
||||
{ |
|
||||
// Use dither by default
|
|
||||
return Quantize(image, bitmapPixelFormat, true); |
|
||||
} |
|
||||
|
|
||||
/// <summary>The quantize.</summary>
|
|
||||
/// <param name="image">The image.</param>
|
|
||||
/// <param name="pixelFormat">The pixel format.</param>
|
|
||||
/// <param name="useDither">The use dither.</param>
|
|
||||
/// <returns>The quantized image with the recalculated color palette.</returns>
|
|
||||
public static Bitmap Quantize(Image image, PixelFormat pixelFormat, bool useDither) |
|
||||
{ |
|
||||
Bitmap tryBitmap = image as Bitmap; |
|
||||
|
|
||||
if (tryBitmap != null && tryBitmap.PixelFormat == PixelFormat.Format32bppArgb) |
|
||||
{ |
|
||||
// The image passed to us is ALREADY a bitmap in the right format. No need to create
|
|
||||
// a copy and work from there.
|
|
||||
return DoQuantize(tryBitmap, pixelFormat, useDither); |
|
||||
} |
|
||||
|
|
||||
// We use these values a lot
|
|
||||
int width = image.Width; |
|
||||
int height = image.Height; |
|
||||
Rectangle sourceRect = Rectangle.FromLTRB(0, 0, width, height); |
|
||||
|
|
||||
// Create a 24-bit rgb version of the source image
|
|
||||
using (Bitmap bitmapSource = new Bitmap(width, height, PixelFormat.Format32bppArgb)) |
|
||||
{ |
|
||||
using (Graphics grfx = Graphics.FromImage(bitmapSource)) |
|
||||
{ |
|
||||
grfx.DrawImage(image, sourceRect, 0, 0, width, height, GraphicsUnit.Pixel); |
|
||||
} |
|
||||
|
|
||||
return DoQuantize(bitmapSource, pixelFormat, useDither); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Does the quantize.
|
|
||||
/// </summary>
|
|
||||
/// <param name="bitmapSource">The bitmap source.</param>
|
|
||||
/// <param name="pixelFormat">The pixel format.</param>
|
|
||||
/// <param name="useDither">if set to <c>true</c> [use dither].</param>
|
|
||||
/// <returns>The quantized image with the recalculated color palette.</returns>
|
|
||||
private static Bitmap DoQuantize(Bitmap bitmapSource, PixelFormat pixelFormat, bool useDither) |
|
||||
{ |
|
||||
// We use these values a lot
|
|
||||
int width = bitmapSource.Width; |
|
||||
int height = bitmapSource.Height; |
|
||||
Rectangle sourceRect = Rectangle.FromLTRB(0, 0, width, height); |
|
||||
|
|
||||
Bitmap bitmapOptimized = null; |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
// Create a bitmap with the same dimensions and the desired format
|
|
||||
bitmapOptimized = new Bitmap(width, height, pixelFormat); |
|
||||
|
|
||||
// Lock the bits of the source image for reading.
|
|
||||
// we will need to write if we do the dither.
|
|
||||
BitmapData bitmapDataSource = bitmapSource.LockBits( |
|
||||
sourceRect, |
|
||||
ImageLockMode.ReadWrite, |
|
||||
PixelFormat.Format32bppArgb); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
// Perform the first pass, which generates the octree data
|
|
||||
// Create an Octree
|
|
||||
Octree octree = new Octree(pixelFormat); |
|
||||
|
|
||||
// Stride might be negative, indicating inverted row order.
|
|
||||
// Allocate a managed buffer for the pixel data, and copy it from the unmanaged pointer.
|
|
||||
int strideSource = Math.Abs(bitmapDataSource.Stride); |
|
||||
byte[] sourceDataBuffer = new byte[strideSource * height]; |
|
||||
Marshal.Copy(bitmapDataSource.Scan0, sourceDataBuffer, 0, sourceDataBuffer.Length); |
|
||||
|
|
||||
// We could skip every other row and/or every other column when sampling the colors
|
|
||||
// of the source image, rather than hitting every other pixel. It doesn't seem to
|
|
||||
// degrade the resulting image too much. But it doesn't really help the performance
|
|
||||
// too much because the majority of the time seems to be spent in other places.
|
|
||||
|
|
||||
// For every row
|
|
||||
int rowStartSource = 0; |
|
||||
for (int ndxRow = 0; ndxRow < height; ndxRow += 1) |
|
||||
{ |
|
||||
// For each column
|
|
||||
for (int ndxCol = 0; ndxCol < width; ndxCol += 1) |
|
||||
{ |
|
||||
// Add the color (4 bytes per pixel - ARGB)
|
|
||||
Pixel pixel = GetSourcePixel(sourceDataBuffer, rowStartSource, ndxCol); |
|
||||
octree.AddColor(pixel); |
|
||||
} |
|
||||
|
|
||||
rowStartSource += strideSource; |
|
||||
} |
|
||||
|
|
||||
// Get the optimized colors
|
|
||||
Color[] colors = octree.GetPaletteColors(); |
|
||||
|
|
||||
// Set the palette from the octree
|
|
||||
ColorPalette palette = bitmapOptimized.Palette; |
|
||||
for (var ndx = 0; ndx < palette.Entries.Length; ++ndx) |
|
||||
{ |
|
||||
// Use the colors we calculated
|
|
||||
// for the rest, just set to transparent
|
|
||||
palette.Entries[ndx] = (ndx < colors.Length) |
|
||||
? colors[ndx] |
|
||||
: Color.Transparent; |
|
||||
} |
|
||||
|
|
||||
bitmapOptimized.Palette = palette; |
|
||||
|
|
||||
// Lock the bits of the optimized bitmap for writing.
|
|
||||
// we will also need to read if we are doing 1bpp or 4bpp
|
|
||||
BitmapData bitmapDataOutput = bitmapOptimized.LockBits(sourceRect, ImageLockMode.ReadWrite, pixelFormat); |
|
||||
try |
|
||||
{ |
|
||||
// Create a managed array for the destination bytes given the desired color depth
|
|
||||
// and marshal the unmanaged data to the managed array
|
|
||||
int strideOutput = Math.Abs(bitmapDataOutput.Stride); |
|
||||
byte[] bitmapOutputBuffer = new byte[strideOutput * height]; |
|
||||
|
|
||||
// For each source pixel, compute the appropriate color index
|
|
||||
rowStartSource = 0; |
|
||||
int rowStartOutput = 0; |
|
||||
|
|
||||
for (int ndxRow = 0; ndxRow < height; ++ndxRow) |
|
||||
{ |
|
||||
// For each column
|
|
||||
for (int ndxCol = 0; ndxCol < width; ++ndxCol) |
|
||||
{ |
|
||||
// Get the source color
|
|
||||
Pixel pixel = GetSourcePixel(sourceDataBuffer, rowStartSource, ndxCol); |
|
||||
|
|
||||
// Get the closest palette index
|
|
||||
int paletteIndex = octree.GetPaletteIndex(pixel); |
|
||||
|
|
||||
// If we want to dither and this isn't the transparent pixel
|
|
||||
if (useDither && pixel.Alpha != 0) |
|
||||
{ |
|
||||
// Calculate the error
|
|
||||
Color paletteColor = colors[paletteIndex]; |
|
||||
int deltaRed = pixel.Red - paletteColor.R; |
|
||||
int deltaGreen = pixel.Green - paletteColor.G; |
|
||||
int deltaBlue = pixel.Blue - paletteColor.B; |
|
||||
|
|
||||
// Propagate the dither error.
|
|
||||
// we'll use a standard Floyd-Steinberg matrix (1/16):
|
|
||||
// | 0 0 0 |
|
|
||||
// | 0 x 7 |
|
|
||||
// | 3 5 1 |
|
|
||||
|
|
||||
// Make sure we're not on the right-hand edge
|
|
||||
if (ndxCol + 1 < width) |
|
||||
{ |
|
||||
DitherSourcePixel(sourceDataBuffer, rowStartSource, ndxCol + 1, deltaRed, deltaGreen, deltaBlue, 7); |
|
||||
} |
|
||||
|
|
||||
// Make sure we're not already on the bottom row
|
|
||||
if (ndxRow + 1 < height) |
|
||||
{ |
|
||||
int nextRow = rowStartSource + strideSource; |
|
||||
|
|
||||
// Make sure we're not on the left-hand column
|
|
||||
if (ndxCol > 0) |
|
||||
{ |
|
||||
// Down one row, but back one pixel
|
|
||||
DitherSourcePixel(sourceDataBuffer, nextRow, ndxCol - 1, deltaRed, deltaGreen, deltaBlue, 3); |
|
||||
} |
|
||||
|
|
||||
// pixel directly below us
|
|
||||
DitherSourcePixel(sourceDataBuffer, nextRow, ndxCol, deltaRed, deltaGreen, deltaBlue, 5); |
|
||||
|
|
||||
// Make sure we're not on the right-hand column
|
|
||||
if (ndxCol + 1 < width) |
|
||||
{ |
|
||||
// Down one row, but right one pixel
|
|
||||
DitherSourcePixel(sourceDataBuffer, nextRow, ndxCol + 1, deltaRed, deltaGreen, deltaBlue, 1); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Set the bitmap index based on the format
|
|
||||
switch (pixelFormat) |
|
||||
{ |
|
||||
case PixelFormat.Format8bppIndexed: |
|
||||
// Each byte is a palette index
|
|
||||
bitmapOutputBuffer[rowStartOutput + ndxCol] = (byte)paletteIndex; |
|
||||
break; |
|
||||
|
|
||||
case PixelFormat.Format4bppIndexed: |
|
||||
// Each byte contains two pixels
|
|
||||
bitmapOutputBuffer[rowStartOutput + (ndxCol >> 1)] |= ((ndxCol & 1) == 1) |
|
||||
? (byte)(paletteIndex & 0x0f) // lower nibble
|
|
||||
: (byte)(paletteIndex << 4); // upper nibble
|
|
||||
break; |
|
||||
|
|
||||
case PixelFormat.Format1bppIndexed: |
|
||||
// Each byte contains eight pixels
|
|
||||
if (paletteIndex != 0) |
|
||||
{ |
|
||||
bitmapOutputBuffer[rowStartOutput + (ndxCol >> 3)] |= (byte)(0x80 >> (ndxCol & 0x07)); |
|
||||
} |
|
||||
|
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
rowStartSource += strideSource; |
|
||||
rowStartOutput += strideOutput; |
|
||||
} |
|
||||
|
|
||||
// Now copy the calculated pixel bytes from the managed array to the unmanaged bitmap
|
|
||||
Marshal.Copy(bitmapOutputBuffer, 0, bitmapDataOutput.Scan0, bitmapOutputBuffer.Length); |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
bitmapOptimized.UnlockBits(bitmapDataOutput); |
|
||||
bitmapDataOutput = null; |
|
||||
} |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
bitmapSource.UnlockBits(bitmapDataSource); |
|
||||
bitmapDataSource = null; |
|
||||
} |
|
||||
} |
|
||||
catch (Exception) |
|
||||
{ |
|
||||
// If any exception is thrown, dispose of the bitmap object
|
|
||||
// we've been working on before we rethrow and bail
|
|
||||
if (bitmapOptimized != null) |
|
||||
{ |
|
||||
bitmapOptimized.Dispose(); |
|
||||
} |
|
||||
|
|
||||
throw; |
|
||||
} |
|
||||
|
|
||||
// Caller is responsible for disposing of this bitmap!
|
|
||||
return bitmapOptimized; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Dithers the source pixel.
|
|
||||
/// </summary>
|
|
||||
/// <param name="buffer">The buffer.</param>
|
|
||||
/// <param name="rowStart">The row start.</param>
|
|
||||
/// <param name="col">The column.</param>
|
|
||||
/// <param name="deltaRed">The delta red.</param>
|
|
||||
/// <param name="deltaGreen">The delta green.</param>
|
|
||||
/// <param name="deltaBlue">The delta blue.</param>
|
|
||||
/// <param name="weight">The weight.</param>
|
|
||||
private static void DitherSourcePixel(byte[] buffer, int rowStart, int col, int deltaRed, int deltaGreen, int deltaBlue, int weight) |
|
||||
{ |
|
||||
int colorIndex = rowStart + (col * 4); |
|
||||
buffer[colorIndex + 2] = ChannelAdjustment(buffer[colorIndex + 2], (deltaRed * weight) >> 4); |
|
||||
buffer[colorIndex + 1] = ChannelAdjustment(buffer[colorIndex + 1], (deltaGreen * weight) >> 4); |
|
||||
buffer[colorIndex] = ChannelAdjustment(buffer[colorIndex], (deltaBlue * weight) >> 4); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the source pixel.
|
|
||||
/// </summary>
|
|
||||
/// <param name="buffer">The buffer.</param>
|
|
||||
/// <param name="rowStart">The row start.</param>
|
|
||||
/// <param name="col">The column.</param>
|
|
||||
/// <returns>The source pixel.</returns>
|
|
||||
private static Pixel GetSourcePixel(byte[] buffer, int rowStart, int col) |
|
||||
{ |
|
||||
int colorIndex = rowStart + (col * 4); |
|
||||
return new Pixel |
|
||||
{ |
|
||||
Alpha = buffer[colorIndex + 3], |
|
||||
Red = buffer[colorIndex + 2], |
|
||||
Green = buffer[colorIndex + 1], |
|
||||
Blue = buffer[colorIndex] |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
#endregion
|
|
||||
|
|
||||
/// <summary>Gets the channel adjustment.</summary>
|
|
||||
/// <param name="current">The current.</param>
|
|
||||
/// <param name="offset">The offset.</param>
|
|
||||
/// <returns>The channel adjustment.</returns>
|
|
||||
private static byte ChannelAdjustment(byte current, int offset) |
|
||||
{ |
|
||||
return (byte)Math.Min(255, Math.Max(0, current + offset)); |
|
||||
} |
|
||||
|
|
||||
#region Octree class
|
|
||||
|
|
||||
/// <summary>data structure for storing and reducing colors used in the source image</summary>
|
|
||||
private class Octree |
|
||||
{ |
|
||||
/// <summary>The m_max colors.</summary>
|
|
||||
private readonly int octreeMaxColors; |
|
||||
|
|
||||
/// <summary>The m_reducible nodes.</summary>
|
|
||||
private readonly OctreeNode[] octreeReducibleNodes; |
|
||||
|
|
||||
/// <summary>The m_color count.</summary>
|
|
||||
private int octreeColorCount; |
|
||||
|
|
||||
/// <summary>The m_has transparent.</summary>
|
|
||||
private bool octreeHasTransparent; |
|
||||
|
|
||||
/// <summary>The m_last argb.</summary>
|
|
||||
private int octreeLastArgb; |
|
||||
|
|
||||
/// <summary>The m_last node.</summary>
|
|
||||
private OctreeNode octreeLastNode; |
|
||||
|
|
||||
/// <summary>The m_palette.</summary>
|
|
||||
private Color[] octreePalette; |
|
||||
|
|
||||
/// <summary>The m_root.</summary>
|
|
||||
private OctreeNode octreeRoot; |
|
||||
|
|
||||
/// <summary>Initializes a new instance of the <see cref="Octree"/> class. Constructor</summary>
|
|
||||
/// <param name="pixelFormat">desired pixel format</param>
|
|
||||
internal Octree(PixelFormat pixelFormat) |
|
||||
{ |
|
||||
// figure out the maximum colors from the pixel format passed in
|
|
||||
switch (pixelFormat) |
|
||||
{ |
|
||||
case PixelFormat.Format1bppIndexed: |
|
||||
this.octreeMaxColors = 2; |
|
||||
break; |
|
||||
|
|
||||
case PixelFormat.Format4bppIndexed: |
|
||||
this.octreeMaxColors = 16; |
|
||||
break; |
|
||||
|
|
||||
case PixelFormat.Format8bppIndexed: |
|
||||
this.octreeMaxColors = 256; |
|
||||
break; |
|
||||
|
|
||||
default: |
|
||||
throw new ArgumentException("Invalid Pixel Format", "pixelFormat"); |
|
||||
} |
|
||||
|
|
||||
// we need a list for each level that may have reducible nodes.
|
|
||||
// since the last level (level 7) is only made up of leaf nodes,
|
|
||||
// we don't need an array entry for it.
|
|
||||
this.octreeReducibleNodes = new OctreeNode[7]; |
|
||||
|
|
||||
// add the initial level-0 root node
|
|
||||
this.octreeRoot = new OctreeNode(0, this); |
|
||||
} |
|
||||
|
|
||||
/// <summary>Add the given pixel color to the octree</summary>
|
|
||||
/// <param name="pixel">points to the pixel color we want to add</param>
|
|
||||
internal void AddColor(Pixel pixel) |
|
||||
{ |
|
||||
// If the A value is non-zero (ignore the transparent color)
|
|
||||
if (pixel.Alpha > 0) |
|
||||
{ |
|
||||
// If we have a previous node and this color is the same as the last...
|
|
||||
if (this.octreeLastNode != null && pixel.Argb == this.octreeLastArgb) |
|
||||
{ |
|
||||
// Just add this color to the same last node
|
|
||||
this.octreeLastNode.AddColor(pixel); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// Just start at the root. If a new color is added,
|
|
||||
// add one to the count (otherwise 0).
|
|
||||
this.octreeColorCount += this.octreeRoot.AddColor(pixel) ? 1 : 0; |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// Flag that we have a transparent color.
|
|
||||
this.octreeHasTransparent = true; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Given a pixel color, return the index of the palette entry
|
|
||||
/// we want to use in the reduced image. If the color is not in the
|
|
||||
/// octree, OctreeNode.GetPaletteIndex will return a negative number.
|
|
||||
/// In that case, we will have to calculate the palette index the brute-force
|
|
||||
/// method by computing the least distance to each color in the palette array.
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">pointer to the pixel color we want to look up</param>
|
|
||||
/// <returns>index of the palette entry we want to use for this color</returns>
|
|
||||
internal int GetPaletteIndex(Pixel pixel) |
|
||||
{ |
|
||||
int paletteIndex = 0; |
|
||||
|
|
||||
// transparent is always the first entry, so if this is transparent,
|
|
||||
// don't do anything.
|
|
||||
if (pixel.Alpha > 0) |
|
||||
{ |
|
||||
paletteIndex = this.octreeRoot.GetPaletteIndex(pixel); |
|
||||
|
|
||||
// returns -1 if this value isn't in the octree.
|
|
||||
if (paletteIndex < 0) |
|
||||
{ |
|
||||
// Use the brute-force method of calculating the closest color
|
|
||||
// in the palette to the one we want
|
|
||||
int minDistance = int.MaxValue; |
|
||||
for (int ndx = 0; ndx < this.octreePalette.Length; ++ndx) |
|
||||
{ |
|
||||
Color paletteColor = this.octreePalette[ndx]; |
|
||||
|
|
||||
// Calculate the delta for each channel
|
|
||||
int deltaRed = pixel.Red - paletteColor.R; |
|
||||
int deltaGreen = pixel.Green - paletteColor.G; |
|
||||
int deltaBlue = pixel.Blue - paletteColor.B; |
|
||||
|
|
||||
// Calculate the distance-squared by summing each channel's square
|
|
||||
int distance = (deltaRed * deltaRed) + (deltaGreen * deltaGreen) + (deltaBlue * deltaBlue); |
|
||||
if (distance < minDistance) |
|
||||
{ |
|
||||
minDistance = distance; |
|
||||
paletteIndex = ndx; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return paletteIndex; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Return a color palette for the computed octree.
|
|
||||
/// </summary>
|
|
||||
/// <returns>A color palette for the computed octree</returns>
|
|
||||
internal Color[] GetPaletteColors() |
|
||||
{ |
|
||||
// If we haven't already computed it, compute it now
|
|
||||
if (this.octreePalette == null) |
|
||||
{ |
|
||||
// Start at the second-to-last reducible level
|
|
||||
int reductionLevel = this.octreeReducibleNodes.Length - 1; |
|
||||
|
|
||||
// We want to subtract one from the target if we have a transparent
|
|
||||
// bit because we want to save room for that special color
|
|
||||
int targetCount = this.octreeMaxColors - (this.octreeHasTransparent ? 1 : 0); |
|
||||
|
|
||||
// While we still have more colors than the target...
|
|
||||
while (this.octreeColorCount > targetCount) |
|
||||
{ |
|
||||
// Find the first reducible node, starting with the last level
|
|
||||
// that can have reducible nodes
|
|
||||
while (reductionLevel > 0 && this.octreeReducibleNodes[reductionLevel] == null) |
|
||||
{ |
|
||||
--reductionLevel; |
|
||||
} |
|
||||
|
|
||||
if (this.octreeReducibleNodes[reductionLevel] == null) |
|
||||
{ |
|
||||
// Shouldn't get here
|
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
// We should have a node now
|
|
||||
OctreeNode newLeaf = this.octreeReducibleNodes[reductionLevel]; |
|
||||
this.octreeReducibleNodes[reductionLevel] = newLeaf.NextReducibleNode; |
|
||||
this.octreeColorCount -= newLeaf.Reduce() - 1; |
|
||||
} |
|
||||
|
|
||||
if (reductionLevel == 0 && !this.octreeHasTransparent) |
|
||||
{ |
|
||||
// If this was the top-most level, we now only have a single color
|
|
||||
// representing the average. That's not what we want.
|
|
||||
// use just black and white
|
|
||||
this.octreePalette = new Color[2]; |
|
||||
this.octreePalette[0] = Color.Black; |
|
||||
this.octreePalette[1] = Color.White; |
|
||||
|
|
||||
// And empty the octree so it always picks the closer of the black and white entries
|
|
||||
this.octreeRoot = new OctreeNode(0, this); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// Now walk the tree, adding all the remaining colors to the list
|
|
||||
int paletteIndex = 0; |
|
||||
this.octreePalette = new Color[this.octreeColorCount + (this.octreeHasTransparent ? 1 : 0)]; |
|
||||
|
|
||||
// Add the transparent color if we need it
|
|
||||
if (this.octreeHasTransparent) |
|
||||
{ |
|
||||
this.octreePalette[paletteIndex++] = Color.Transparent; |
|
||||
} |
|
||||
|
|
||||
// Have the nodes insert their leaf colors
|
|
||||
this.octreeRoot.AddColorsToPalette(this.octreePalette, ref paletteIndex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return this.octreePalette; |
|
||||
} |
|
||||
|
|
||||
/// <summary>set up the values we need to reuse the given pointer if the next color is argb</summary>
|
|
||||
/// <param name="node">last node to which we added a color</param>
|
|
||||
/// <param name="argb">last color we added</param>
|
|
||||
private void SetLastNode(OctreeNode node, int argb) |
|
||||
{ |
|
||||
this.octreeLastNode = node; |
|
||||
this.octreeLastArgb = argb; |
|
||||
} |
|
||||
|
|
||||
/// <summary>When a reducible node is added, this method is called to add it to the appropriate
|
|
||||
/// reducible node list (given its level)</summary>
|
|
||||
/// <param name="reducibleNode">node to add to a reducible list</param>
|
|
||||
private void AddReducibleNode(OctreeNode reducibleNode) |
|
||||
{ |
|
||||
// hook this node into the front of the list.
|
|
||||
// this means the last one added will be the first in the list.
|
|
||||
reducibleNode.NextReducibleNode = this.octreeReducibleNodes[reducibleNode.Level]; |
|
||||
this.octreeReducibleNodes[reducibleNode.Level] = reducibleNode; |
|
||||
} |
|
||||
|
|
||||
#region OctreeNode class
|
|
||||
|
|
||||
/// <summary>Node for an Octree structure</summary>
|
|
||||
private class OctreeNode |
|
||||
{ |
|
||||
/// <summary>The s_level masks.</summary>
|
|
||||
private static readonly byte[] NodeLevelMasks = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; |
|
||||
|
|
||||
/// <summary>The m_level.</summary>
|
|
||||
private readonly int nodeLevel; |
|
||||
|
|
||||
/// <summary>The m_octree.</summary>
|
|
||||
private readonly Octree nodeOctree; |
|
||||
|
|
||||
/// <summary>The m_blue sum.</summary>
|
|
||||
private int nodeBlueSum; |
|
||||
|
|
||||
/// <summary>The m_child nodes.</summary>
|
|
||||
private OctreeNode[] nodeChildNodes; |
|
||||
|
|
||||
/// <summary>The m_green sum.</summary>
|
|
||||
private int nodeGreenSum; |
|
||||
|
|
||||
/// <summary>The m_is leaf.</summary>
|
|
||||
private bool nodeIsLeaf; |
|
||||
|
|
||||
/// <summary>The m_palette index.</summary>
|
|
||||
private int nodePaletteIndex; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// The pixel count.Information we need to calculate the average color for a set of pixels
|
|
||||
/// </summary>
|
|
||||
private int nodePixelCount; |
|
||||
|
|
||||
/// <summary>The m_red sum.</summary>
|
|
||||
private int nodeRedSum; |
|
||||
|
|
||||
/// <summary>Initializes a new instance of the <see cref="OctreeNode"/> class. Constructor</summary>
|
|
||||
/// <param name="level">level for this node</param>
|
|
||||
/// <param name="octree">owning octree</param>
|
|
||||
internal OctreeNode(int level, Octree octree) |
|
||||
{ |
|
||||
this.nodeOctree = octree; |
|
||||
this.nodeLevel = level; |
|
||||
|
|
||||
// Since there are only eight levels, if we get to level 7
|
|
||||
// We automatically make this a leaf node
|
|
||||
this.nodeIsLeaf = level == 7; |
|
||||
|
|
||||
if (!this.nodeIsLeaf) |
|
||||
{ |
|
||||
// Create the child array
|
|
||||
this.nodeChildNodes = new OctreeNode[8]; |
|
||||
|
|
||||
// Add it to the tree's reducible node list
|
|
||||
this.nodeOctree.AddReducibleNode(this); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>Gets Level.</summary>
|
|
||||
internal int Level |
|
||||
{ |
|
||||
get { return this.nodeLevel; } |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets NextReducibleNode.
|
|
||||
/// Once we compute a palette, this will be set
|
|
||||
/// to the palette index associated with this leaf node.
|
|
||||
/// Nodes are arranged in linked lists of reducible nodes for a given level.
|
|
||||
/// this field and property is used to traverse that list.
|
|
||||
/// </summary>
|
|
||||
internal OctreeNode NextReducibleNode { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the average color for this node.
|
|
||||
/// </summary>
|
|
||||
private Color NodeColor |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
// Average color is the sum of each channel divided by the pixel count
|
|
||||
return Color.FromArgb( |
|
||||
this.nodeRedSum / this.nodePixelCount, |
|
||||
this.nodeGreenSum / this.nodePixelCount, |
|
||||
this.nodeBlueSum / this.nodePixelCount); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Add the given color to this node if it is a leaf, otherwise recurse
|
|
||||
/// down the appropriate child
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">color to add</param>
|
|
||||
/// <returns>true if a new color was added to the octree</returns>
|
|
||||
internal bool AddColor(Pixel pixel) |
|
||||
{ |
|
||||
bool colorAdded; |
|
||||
if (this.nodeIsLeaf) |
|
||||
{ |
|
||||
// Increase the pixel count for this node, and if
|
|
||||
// the result is 1, then this is a new color
|
|
||||
colorAdded = ++this.nodePixelCount == 1; |
|
||||
|
|
||||
// Add the color to the running sum for this node
|
|
||||
this.nodeRedSum += pixel.Red; |
|
||||
this.nodeGreenSum += pixel.Green; |
|
||||
this.nodeBlueSum += pixel.Blue; |
|
||||
|
|
||||
// Set the last node so we can quickly process adjacent pixels
|
|
||||
// with the same color
|
|
||||
this.nodeOctree.SetLastNode(this, pixel.Argb); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// Get the index at this level for the rgb values
|
|
||||
int childIndex = this.GetChildIndex(pixel); |
|
||||
|
|
||||
// If there is no child, add one now to the next level
|
|
||||
if (this.nodeChildNodes[childIndex] == null) |
|
||||
{ |
|
||||
this.nodeChildNodes[childIndex] = new OctreeNode(this.nodeLevel + 1, this.nodeOctree); |
|
||||
} |
|
||||
|
|
||||
// Recurse...
|
|
||||
colorAdded = this.nodeChildNodes[childIndex].AddColor(pixel); |
|
||||
} |
|
||||
|
|
||||
return colorAdded; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Given a source color, return the palette index to use for the reduced image.
|
|
||||
/// Returns -1 if the color is not represented in the octree (this happens if
|
|
||||
/// the color has been dithered into a new color that did not appear in the
|
|
||||
/// original image when the octree was formed in pass 1.
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">source color to look up</param>
|
|
||||
/// <returns>palette index to use</returns>
|
|
||||
internal int GetPaletteIndex(Pixel pixel) |
|
||||
{ |
|
||||
int paletteIndex = -1; |
|
||||
if (this.nodeIsLeaf) |
|
||||
{ |
|
||||
// Use this leaf node's palette index
|
|
||||
paletteIndex = this.nodePaletteIndex; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// Get the index at this level for the rgb values
|
|
||||
var childIndex = this.GetChildIndex(pixel); |
|
||||
if (this.nodeChildNodes[childIndex] != null) |
|
||||
{ |
|
||||
// Recurse...
|
|
||||
paletteIndex = this.nodeChildNodes[childIndex].GetPaletteIndex(pixel); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return paletteIndex; |
|
||||
} |
|
||||
|
|
||||
/// <summary>Reduce this node by combining all child nodes</summary>
|
|
||||
/// <returns>number of nodes removed</returns>
|
|
||||
internal int Reduce() |
|
||||
{ |
|
||||
int numReduced = 0; |
|
||||
if (!this.nodeIsLeaf) |
|
||||
{ |
|
||||
// For each child
|
|
||||
foreach (OctreeNode node in this.nodeChildNodes) |
|
||||
{ |
|
||||
if (node != null) |
|
||||
{ |
|
||||
OctreeNode childNode = node; |
|
||||
|
|
||||
// add the pixel count from the child
|
|
||||
this.nodePixelCount += childNode.nodePixelCount; |
|
||||
|
|
||||
// add the running color sums from the child
|
|
||||
this.nodeRedSum += childNode.nodeRedSum; |
|
||||
this.nodeGreenSum += childNode.nodeGreenSum; |
|
||||
this.nodeBlueSum += childNode.nodeBlueSum; |
|
||||
|
|
||||
++numReduced; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
this.nodeChildNodes = null; |
|
||||
this.nodeIsLeaf = true; |
|
||||
} |
|
||||
|
|
||||
return numReduced; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// If this is a leaf node, add its color to the palette array at the given index
|
|
||||
/// and increment the index. If not a leaf, recurse the children nodes.
|
|
||||
/// </summary>
|
|
||||
/// <param name="colorArray">array of colors</param>
|
|
||||
/// <param name="paletteIndex">index of the next empty slot in the array</param>
|
|
||||
internal void AddColorsToPalette(Color[] colorArray, ref int paletteIndex) |
|
||||
{ |
|
||||
if (this.nodeIsLeaf) |
|
||||
{ |
|
||||
// Save our index and increment the running index
|
|
||||
this.nodePaletteIndex = paletteIndex++; |
|
||||
|
|
||||
// The color for this node is the average color, which is created by
|
|
||||
// dividing the running sums for each channel by the pixel count
|
|
||||
colorArray[this.nodePaletteIndex] = this.NodeColor; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
// Just run through all the non-null children and recurse
|
|
||||
foreach (OctreeNode node in this.nodeChildNodes) |
|
||||
{ |
|
||||
if (node != null) |
|
||||
{ |
|
||||
node.AddColorsToPalette(colorArray, ref paletteIndex); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Return the child index for a given color.
|
|
||||
/// Depends on which level this node is in.
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">color pixel to compute</param>
|
|
||||
/// <returns>child index (0-7)</returns>
|
|
||||
private int GetChildIndex(Pixel pixel) |
|
||||
{ |
|
||||
// lvl: 0 1 2 3 4 5 6 7
|
|
||||
// bit: 7 6 5 4 3 2 1 0
|
|
||||
var shift = 7 - this.nodeLevel; |
|
||||
int mask = NodeLevelMasks[this.nodeLevel]; |
|
||||
return ((pixel.Red & mask) >> (shift - 2)) | |
|
||||
((pixel.Green & mask) >> (shift - 1)) | |
|
||||
((pixel.Blue & mask) >> shift); |
|
||||
} |
|
||||
} |
|
||||
#endregion
|
|
||||
} |
|
||||
#endregion
|
|
||||
|
|
||||
#region Pixel class for ARGB values
|
|
||||
/// <summary>
|
|
||||
/// Structure of a Format32bppArgb pixel in memory.
|
|
||||
/// </summary>
|
|
||||
private class Pixel |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Gets or sets the blue component of the pixel.
|
|
||||
/// </summary>
|
|
||||
public byte Blue { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the green component of the pixel.
|
|
||||
/// </summary>
|
|
||||
public byte Green { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the red component of the pixel.
|
|
||||
/// </summary>
|
|
||||
public byte Red { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the alpha component of the pixel.
|
|
||||
/// </summary>
|
|
||||
public byte Alpha { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the argb combination of the pixel.
|
|
||||
/// </summary>
|
|
||||
public int Argb |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
return this.Alpha << 24 | this.Red << 16 | this.Green << 8 | this.Blue; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
#endregion
|
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,558 @@ |
|||||
|
// --------------------------------------------------------------------------------------------------------------------
|
||||
|
// <copyright file="OctreeQuantizer.cs" company="James South">
|
||||
|
// Copyright (c) James South.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// </copyright>
|
||||
|
// <summary>
|
||||
|
// Encapsulates methods to calculate the colour palette if an image using an octree pattern.
|
||||
|
// </summary>
|
||||
|
// --------------------------------------------------------------------------------------------------------------------
|
||||
|
|
||||
|
namespace ImageProcessor.Imaging |
||||
|
{ |
||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Drawing; |
||||
|
using System.Drawing.Imaging; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encapsulates methods to calculate the colour palette if an image using an octree pattern.
|
||||
|
/// </summary>
|
||||
|
public class OctreeQuantizer : Quantizer |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Stores the tree.
|
||||
|
/// </summary>
|
||||
|
private readonly Octree octree; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The maximum allowed color depth.
|
||||
|
/// </summary>
|
||||
|
private readonly int maxColors; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// The Octree quantizer is a two pass algorithm. The initial pass sets up the octree,
|
||||
|
/// the second pass quantizes a color based on the nodes in the tree
|
||||
|
/// </remarks>
|
||||
|
/// <param name="maxColors">
|
||||
|
/// The maximum number of colors to return
|
||||
|
/// </param>
|
||||
|
/// <param name="maxColorBits">
|
||||
|
/// The number of significant bits
|
||||
|
/// </param>
|
||||
|
public OctreeQuantizer(int maxColors, int maxColorBits) |
||||
|
: base(false) |
||||
|
{ |
||||
|
if (maxColors > 255) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException("maxColors", maxColors, "The number of colors should be less than 256"); |
||||
|
} |
||||
|
|
||||
|
if ((maxColorBits < 1) | (maxColorBits > 8)) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException("maxColorBits", maxColorBits, "This should be between 1 and 8"); |
||||
|
} |
||||
|
|
||||
|
// Construct the octree
|
||||
|
this.octree = new Octree(maxColorBits); |
||||
|
this.maxColors = maxColors; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Override this to process the pixel in the first pass of the algorithm
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">The pixel to quantize</param>
|
||||
|
/// <remarks>
|
||||
|
/// This function need only be overridden if your quantize algorithm needs two passes,
|
||||
|
/// such as an Octree quantizer.
|
||||
|
/// </remarks>
|
||||
|
protected override void InitialQuantizePixel(Color32 pixel) |
||||
|
{ |
||||
|
// Add the color to the octree
|
||||
|
this.octree.AddColor(pixel); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Override this to process the pixel in the second pass of the algorithm
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">The pixel to quantize</param>
|
||||
|
/// <returns>
|
||||
|
/// The quantized value
|
||||
|
/// </returns>
|
||||
|
protected override byte QuantizePixel(Color32 pixel) |
||||
|
{ |
||||
|
// The color at [_maxColors] is set to transparent
|
||||
|
byte paletteIndex = (byte)this.maxColors; |
||||
|
|
||||
|
// Get the palette index if this non-transparent
|
||||
|
if (pixel.Alpha > 0) |
||||
|
{ |
||||
|
paletteIndex = (byte)this.octree.GetPaletteIndex(pixel); |
||||
|
} |
||||
|
|
||||
|
return paletteIndex; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieve the palette for the quantized image
|
||||
|
/// </summary>
|
||||
|
/// <param name="original">Any old palette, this is overwritten</param>
|
||||
|
/// <returns>
|
||||
|
/// The new color palette
|
||||
|
/// </returns>
|
||||
|
protected override ColorPalette GetPalette(ColorPalette original) |
||||
|
{ |
||||
|
// First off convert the octree to _maxColors colors
|
||||
|
ArrayList palette = this.octree.Palletize(this.maxColors - 1); |
||||
|
|
||||
|
// Then convert the palette based on those colors
|
||||
|
for (int index = 0; index < palette.Count; index++) |
||||
|
{ |
||||
|
Color testColor = (Color)palette[index]; |
||||
|
|
||||
|
// Test set transparent color when color transparency used
|
||||
|
if (testColor.ToArgb() == Color.Transparent.ToArgb()) |
||||
|
{ |
||||
|
testColor = Color.FromArgb(0, 0, 0, 0); |
||||
|
} |
||||
|
|
||||
|
original.Entries[index] = testColor; |
||||
|
} |
||||
|
|
||||
|
// Clear unused palette entries
|
||||
|
for (int index = palette.Count; index < this.maxColors; index++) |
||||
|
{ |
||||
|
original.Entries[index] = Color.FromArgb(255, 0, 0, 0); |
||||
|
} |
||||
|
|
||||
|
// Add the transparent color when alpha transparency used
|
||||
|
original.Entries[this.maxColors] = Color.FromArgb(0, Color.Transparent); |
||||
|
return original; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Describes a tree data structure in which each internal node has exactly eight children.
|
||||
|
/// </summary>
|
||||
|
private class Octree |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Mask used when getting the appropriate pixels for a given node
|
||||
|
/// </summary>
|
||||
|
private static readonly int[] Mask = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The root of the octree
|
||||
|
/// </summary>
|
||||
|
private readonly OctreeNode root; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Array of reducible nodes
|
||||
|
/// </summary>
|
||||
|
private readonly OctreeNode[] reducibleNodes; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Maximum number of significant bits in the image
|
||||
|
/// </summary>
|
||||
|
private readonly int maxColorBits; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Number of leaves in the tree
|
||||
|
/// </summary>
|
||||
|
private int leafCount; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Store the last node quantized
|
||||
|
/// </summary>
|
||||
|
private OctreeNode previousNode; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Cache the previous color quantized
|
||||
|
/// </summary>
|
||||
|
private int previousColor; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="Octree"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="maxColorBits">
|
||||
|
/// The maximum number of significant bits in the image
|
||||
|
/// </param>
|
||||
|
public Octree(int maxColorBits) |
||||
|
{ |
||||
|
this.maxColorBits = maxColorBits; |
||||
|
this.leafCount = 0; |
||||
|
this.reducibleNodes = new OctreeNode[9]; |
||||
|
this.root = new OctreeNode(0, this.maxColorBits, this); |
||||
|
this.previousColor = 0; |
||||
|
this.previousNode = null; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the number of leaves in the tree
|
||||
|
/// </summary>
|
||||
|
private int Leaves |
||||
|
{ |
||||
|
get { return this.leafCount; } |
||||
|
set { this.leafCount = value; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Add a given colour value to the octree
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">
|
||||
|
/// The color value to add.
|
||||
|
/// </param>
|
||||
|
public void AddColor(Color32 pixel) |
||||
|
{ |
||||
|
// Check if this request is for the same color as the last
|
||||
|
if (this.previousColor == pixel.Argb) |
||||
|
{ |
||||
|
// If so, check if I have a previous node setup. This will only ocurr if the first color in the image
|
||||
|
// happens to be black, with an alpha component of zero.
|
||||
|
if (null == this.previousNode) |
||||
|
{ |
||||
|
this.previousColor = pixel.Argb; |
||||
|
this.root.AddColor(pixel, this.maxColorBits, 0, this); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Just update the previous node
|
||||
|
this.previousNode.Increment(pixel); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
this.previousColor = pixel.Argb; |
||||
|
this.root.AddColor(pixel, this.maxColorBits, 0, this); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Convert the nodes in the octree to a palette with a maximum of colorCount colors
|
||||
|
/// </summary>
|
||||
|
/// <param name="colorCount">The maximum number of colors</param>
|
||||
|
/// <returns>An array-list with the palletized colors</returns>
|
||||
|
public ArrayList Palletize(int colorCount) |
||||
|
{ |
||||
|
while (this.Leaves > colorCount) |
||||
|
{ |
||||
|
this.Reduce(); |
||||
|
} |
||||
|
|
||||
|
// Now palletized the nodes
|
||||
|
ArrayList palette = new ArrayList(this.Leaves); |
||||
|
int paletteIndex = 0; |
||||
|
this.root.ConstructPalette(palette, ref paletteIndex); |
||||
|
|
||||
|
// And return the palette
|
||||
|
return palette; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the palette index for the passed colour.
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">
|
||||
|
/// The color to return the palette index for.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// The palette index for the passed colour.
|
||||
|
/// </returns>
|
||||
|
public int GetPaletteIndex(Color32 pixel) |
||||
|
{ |
||||
|
return this.root.GetPaletteIndex(pixel, 0); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Return the array of reducible nodes
|
||||
|
/// </summary>
|
||||
|
/// <returns>
|
||||
|
/// The array of <see cref="OctreeNode"/>.
|
||||
|
/// </returns>
|
||||
|
protected OctreeNode[] ReducibleNodes() |
||||
|
{ |
||||
|
return this.reducibleNodes; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Keep track of the previous node that was quantized
|
||||
|
/// </summary>
|
||||
|
/// <param name="node">The node last quantized</param>
|
||||
|
protected void TrackPrevious(OctreeNode node) |
||||
|
{ |
||||
|
this.previousNode = node; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reduce the depth of the tree
|
||||
|
/// </summary>
|
||||
|
private void Reduce() |
||||
|
{ |
||||
|
// Find the deepest level containing at least one reducible node
|
||||
|
// for (index = _maxColorBits - 1; (index > 0) && (null == _reducibleNodes[index]); index--) ;
|
||||
|
// Find the deepest level containing at least one reducible node
|
||||
|
int index = this.maxColorBits - 1; |
||||
|
while ((index > 0) && (null == this.reducibleNodes[index])) |
||||
|
{ |
||||
|
index--; |
||||
|
} |
||||
|
|
||||
|
// Reduce the node most recently added to the list at level 'index'
|
||||
|
OctreeNode node = this.reducibleNodes[index]; |
||||
|
this.reducibleNodes[index] = node.NextReducible; |
||||
|
|
||||
|
// Decrement the leaf count after reducing the node
|
||||
|
this.leafCount -= node.Reduce(); |
||||
|
|
||||
|
// And just in case I've reduced the last color to be added, and the next color to
|
||||
|
// be added is the same, invalidate the previousNode...
|
||||
|
this.previousNode = null; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Class which encapsulates each node in the tree
|
||||
|
/// </summary>
|
||||
|
protected class OctreeNode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Pointers to any child nodes
|
||||
|
/// </summary>
|
||||
|
private readonly OctreeNode[] children; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Pointer to next reducible node
|
||||
|
/// </summary>
|
||||
|
private readonly OctreeNode nextReducible; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Flag indicating that this is a leaf node
|
||||
|
/// </summary>
|
||||
|
private bool leaf; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Number of pixels in this node
|
||||
|
/// </summary>
|
||||
|
private int pixelCount; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Red component
|
||||
|
/// </summary>
|
||||
|
private int red; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Green Component
|
||||
|
/// </summary>
|
||||
|
private int green; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Blue component
|
||||
|
/// </summary>
|
||||
|
private int blue; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The index of this node in the palette
|
||||
|
/// </summary>
|
||||
|
private int paletteIndex; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="OctreeNode"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="level">
|
||||
|
/// The level in the tree = 0 - 7
|
||||
|
/// </param>
|
||||
|
/// <param name="colorBits">
|
||||
|
/// The number of significant color bits in the image
|
||||
|
/// </param>
|
||||
|
/// <param name="octree">
|
||||
|
/// The tree to which this node belongs
|
||||
|
/// </param>
|
||||
|
public OctreeNode(int level, int colorBits, Octree octree) |
||||
|
{ |
||||
|
// Construct the new node
|
||||
|
this.leaf = level == colorBits; |
||||
|
|
||||
|
this.red = this.green = this.blue = 0; |
||||
|
this.pixelCount = 0; |
||||
|
|
||||
|
// If a leaf, increment the leaf count
|
||||
|
if (this.leaf) |
||||
|
{ |
||||
|
octree.Leaves++; |
||||
|
this.nextReducible = null; |
||||
|
this.children = null; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Otherwise add this to the reducible nodes
|
||||
|
var repNodes = octree.ReducibleNodes(); |
||||
|
this.nextReducible = repNodes[level]; |
||||
|
repNodes[level] = this; |
||||
|
this.children = new OctreeNode[8]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the next reducible node.
|
||||
|
/// </summary>
|
||||
|
public OctreeNode NextReducible |
||||
|
{ |
||||
|
get { return this.nextReducible; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Add a color into the tree
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">The color</param>
|
||||
|
/// <param name="colorBits">The number of significant color bits</param>
|
||||
|
/// <param name="level">The level in the tree</param>
|
||||
|
/// <param name="octree">The tree to which this node belongs</param>
|
||||
|
public void AddColor(Color32 pixel, int colorBits, int level, Octree octree) |
||||
|
{ |
||||
|
// Update the color information if this is a leaf
|
||||
|
if (this.leaf) |
||||
|
{ |
||||
|
this.Increment(pixel); |
||||
|
|
||||
|
// Setup the previous node
|
||||
|
octree.TrackPrevious(this); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
checked |
||||
|
{ |
||||
|
// Go to the next level down in the tree
|
||||
|
int shift = 7 - level; |
||||
|
int index = ((pixel.Red & Mask[level]) >> (shift - 2)) | |
||||
|
((pixel.Green & Mask[level]) >> (shift - 1)) | |
||||
|
((pixel.Blue & Mask[level]) >> shift); |
||||
|
|
||||
|
OctreeNode child = this.children[index]; |
||||
|
|
||||
|
if (null == child) |
||||
|
{ |
||||
|
// Create a new child node and store in the array
|
||||
|
child = new OctreeNode(level + 1, colorBits, octree); |
||||
|
this.children[index] = child; |
||||
|
} |
||||
|
|
||||
|
// Add the color to the child node
|
||||
|
child.AddColor(pixel, colorBits, level + 1, octree); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reduce this node by removing all of its children
|
||||
|
/// </summary>
|
||||
|
/// <returns>The number of leaves removed</returns>
|
||||
|
public int Reduce() |
||||
|
{ |
||||
|
this.red = this.green = this.blue = 0; |
||||
|
int childNodes = 0; |
||||
|
|
||||
|
// Loop through all children and add their information to this node
|
||||
|
for (int index = 0; index < 8; index++) |
||||
|
{ |
||||
|
if (null != this.children[index]) |
||||
|
{ |
||||
|
this.red += this.children[index].red; |
||||
|
this.green += this.children[index].green; |
||||
|
this.blue += this.children[index].blue; |
||||
|
this.pixelCount += this.children[index].pixelCount; |
||||
|
++childNodes; |
||||
|
this.children[index] = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Now change this to a leaf node
|
||||
|
this.leaf = true; |
||||
|
|
||||
|
// Return the number of nodes to decrement the leaf count by
|
||||
|
return childNodes - 1; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Traverse the tree, building up the color palette
|
||||
|
/// </summary>
|
||||
|
/// <param name="palette">The palette</param>
|
||||
|
/// <param name="index">The current palette index</param>
|
||||
|
public void ConstructPalette(ArrayList palette, ref int index) |
||||
|
{ |
||||
|
if (this.leaf) |
||||
|
{ |
||||
|
// Consume the next palette index
|
||||
|
this.paletteIndex = index++; |
||||
|
|
||||
|
// And set the color of the palette entry
|
||||
|
palette.Add(Color.FromArgb(this.red / this.pixelCount, this.green / this.pixelCount, this.blue / this.pixelCount)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Loop through children looking for leaves
|
||||
|
for (int i = 0; i < 8; i++) |
||||
|
{ |
||||
|
if (null != this.children[i]) |
||||
|
{ |
||||
|
this.children[i].ConstructPalette(palette, ref index); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Return the palette index for the passed color.
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">
|
||||
|
/// The pixel.
|
||||
|
/// </param>
|
||||
|
/// <param name="level">
|
||||
|
/// The level.
|
||||
|
/// </param>
|
||||
|
/// <returns>
|
||||
|
/// The palette index for the passed color.
|
||||
|
/// </returns>
|
||||
|
public int GetPaletteIndex(Color32 pixel, int level) |
||||
|
{ |
||||
|
int index = this.paletteIndex; |
||||
|
|
||||
|
if (!this.leaf) |
||||
|
{ |
||||
|
checked |
||||
|
{ |
||||
|
int shift = 7 - level; |
||||
|
int i = ((pixel.Red & Mask[level]) >> (shift - 2)) |
||||
|
| ((pixel.Green & Mask[level]) >> (shift - 1)) |
||||
|
| ((pixel.Blue & Mask[level]) >> shift); |
||||
|
|
||||
|
if (null != this.children[i]) |
||||
|
{ |
||||
|
index = this.children[i].GetPaletteIndex(pixel, level + 1); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new ArgumentException("Didn't expect this!"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return index; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Increment the pixel count and add to the color information
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">
|
||||
|
/// The pixel.
|
||||
|
/// </param>
|
||||
|
public void Increment(Color32 pixel) |
||||
|
{ |
||||
|
this.pixelCount++; |
||||
|
this.red += pixel.Red; |
||||
|
this.green += pixel.Green; |
||||
|
this.blue += pixel.Blue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,350 @@ |
|||||
|
// --------------------------------------------------------------------------------------------------------------------
|
||||
|
// <copyright file="Quantizer.cs" company="James South">
|
||||
|
// Copyright (c) James South.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
// </copyright>
|
||||
|
// <summary>
|
||||
|
// Defines the Quantizer type.
|
||||
|
// </summary>
|
||||
|
// --------------------------------------------------------------------------------------------------------------------
|
||||
|
|
||||
|
namespace ImageProcessor.Imaging |
||||
|
{ |
||||
|
using System; |
||||
|
using System.Drawing; |
||||
|
using System.Drawing.Imaging; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encapsulates methods to calculate the color palette of an image.
|
||||
|
/// </summary>
|
||||
|
public abstract class Quantizer |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The flag used to indicate whether a single pass or two passes are needed for quantization.
|
||||
|
/// </summary>
|
||||
|
private readonly bool singlePass; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The size in bytes of the 32 bytes per pixel Color structure.
|
||||
|
/// </summary>
|
||||
|
private readonly int pixelSize; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="T:ImageProcessor.Imaging.Quantizer">Quantizer</see> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="singlePass">
|
||||
|
/// If set to <see langword="true"/>, then the quantizer will loop through the source pixels once;
|
||||
|
/// otherwise, <see langword="false"/>.
|
||||
|
/// </param>
|
||||
|
protected Quantizer(bool singlePass) |
||||
|
{ |
||||
|
this.singlePass = singlePass; |
||||
|
this.pixelSize = Marshal.SizeOf(typeof(Color32)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Quantizes the given <see cref="T:System.Drawing.Image">Image</see> and returns the resulting output
|
||||
|
/// <see cref="T:System.Drawing.Bitmap">Bitmap.</see>
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The image to quantize</param>
|
||||
|
/// <returns>
|
||||
|
/// A quantized <see cref="T:System.Drawing.Bitmap">Bitmap</see> version of the <see cref="T:System.Drawing.Image">Image</see>
|
||||
|
/// </returns>
|
||||
|
public Bitmap Quantize(Image source) |
||||
|
{ |
||||
|
// Get the size of the source image
|
||||
|
int height = source.Height; |
||||
|
int width = source.Width; |
||||
|
|
||||
|
// And construct a rectangle from these dimensions
|
||||
|
Rectangle bounds = new Rectangle(0, 0, width, height); |
||||
|
|
||||
|
// First off take a 32bpp copy of the image
|
||||
|
Bitmap copy = new Bitmap(width, height, PixelFormat.Format32bppArgb); |
||||
|
|
||||
|
// And construct an 8bpp version
|
||||
|
Bitmap output = new Bitmap(width, height, PixelFormat.Format8bppIndexed); |
||||
|
|
||||
|
// Now lock the bitmap into memory
|
||||
|
using (Graphics g = Graphics.FromImage(copy)) |
||||
|
{ |
||||
|
g.PageUnit = GraphicsUnit.Pixel; |
||||
|
|
||||
|
// Draw the source image onto the copy bitmap,
|
||||
|
// which will effect a widening as appropriate.
|
||||
|
g.DrawImage(source, bounds); |
||||
|
} |
||||
|
|
||||
|
// Define a pointer to the bitmap data
|
||||
|
BitmapData sourceData = null; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
// Get the source image bits and lock into memory
|
||||
|
sourceData = copy.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); |
||||
|
|
||||
|
// Call the FirstPass function if not a single pass algorithm.
|
||||
|
// For something like an octree quantizer, this will run through
|
||||
|
// all image pixels, build a data structure, and create a palette.
|
||||
|
if (!this.singlePass) |
||||
|
{ |
||||
|
this.FirstPass(sourceData, width, height); |
||||
|
} |
||||
|
|
||||
|
// Then set the color palette on the output bitmap. I'm passing in the current palette
|
||||
|
// as there's no way to construct a new, empty palette.
|
||||
|
output.Palette = this.GetPalette(output.Palette); |
||||
|
|
||||
|
// Then call the second pass which actually does the conversion
|
||||
|
this.SecondPass(sourceData, output, width, height, bounds); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
// Ensure that the bits are unlocked
|
||||
|
copy.UnlockBits(sourceData); |
||||
|
} |
||||
|
|
||||
|
// Last but not least, return the output bitmap
|
||||
|
return output; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Execute the first pass through the pixels in the image
|
||||
|
/// </summary>
|
||||
|
/// <param name="sourceData">The source data</param>
|
||||
|
/// <param name="width">The width in pixels of the image</param>
|
||||
|
/// <param name="height">The height in pixels of the image</param>
|
||||
|
protected virtual void FirstPass(BitmapData sourceData, int width, int height) |
||||
|
{ |
||||
|
// Define the source data pointers. The source row is a byte to
|
||||
|
// keep addition of the stride value easier (as this is in bytes)
|
||||
|
IntPtr sourceRow = sourceData.Scan0; |
||||
|
|
||||
|
// Loop through each row
|
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
// Set the source pixel to the first pixel in this row
|
||||
|
IntPtr sourcePixel = sourceRow; |
||||
|
|
||||
|
// And loop through each column
|
||||
|
for (int col = 0; col < width; col++) |
||||
|
{ |
||||
|
this.InitialQuantizePixel(new Color32(sourcePixel)); |
||||
|
sourcePixel = (IntPtr)((int)sourcePixel + this.pixelSize); |
||||
|
} |
||||
|
|
||||
|
// Now I have the pixel, call the FirstPassQuantize function...
|
||||
|
// Add the stride to the source row
|
||||
|
sourceRow = (IntPtr)((long)sourceRow + sourceData.Stride); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Execute a second pass through the bitmap
|
||||
|
/// </summary>
|
||||
|
/// <param name="sourceData">The source bitmap, locked into memory</param>
|
||||
|
/// <param name="output">The output bitmap</param>
|
||||
|
/// <param name="width">The width in pixels of the image</param>
|
||||
|
/// <param name="height">The height in pixels of the image</param>
|
||||
|
/// <param name="bounds">The bounding rectangle</param>
|
||||
|
protected virtual void SecondPass(BitmapData sourceData, Bitmap output, int width, int height, Rectangle bounds) |
||||
|
{ |
||||
|
BitmapData outputData = null; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
// Lock the output bitmap into memory
|
||||
|
outputData = output.LockBits(bounds, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); |
||||
|
|
||||
|
// Define the source data pointers. The source row is a byte to
|
||||
|
// keep addition of the stride value easier (as this is in bytes)
|
||||
|
IntPtr sourceRow = sourceData.Scan0; |
||||
|
IntPtr sourcePixel = sourceRow; |
||||
|
IntPtr previousPixel = sourcePixel; |
||||
|
|
||||
|
// Now define the destination data pointers
|
||||
|
IntPtr destinationRow = outputData.Scan0; |
||||
|
IntPtr destinationPixel = destinationRow; |
||||
|
|
||||
|
// And convert the first pixel, so that I have values going into the loop.
|
||||
|
byte pixelValue = this.QuantizePixel(new Color32(sourcePixel)); |
||||
|
|
||||
|
// Assign the value of the first pixel
|
||||
|
Marshal.WriteByte(destinationPixel, pixelValue); |
||||
|
|
||||
|
// Loop through each row
|
||||
|
for (int row = 0; row < height; row++) |
||||
|
{ |
||||
|
// Set the source pixel to the first pixel in this row
|
||||
|
sourcePixel = sourceRow; |
||||
|
|
||||
|
// And set the destination pixel pointer to the first pixel in the row
|
||||
|
destinationPixel = destinationRow; |
||||
|
|
||||
|
// Loop through each pixel on this scan line
|
||||
|
for (int col = 0; col < width; col++) |
||||
|
{ |
||||
|
// Check if this is the same as the last pixel. If so use that value
|
||||
|
// rather than calculating it again. This is an inexpensive optimisation.
|
||||
|
if (Marshal.ReadInt32(previousPixel) != Marshal.ReadInt32(sourcePixel)) |
||||
|
{ |
||||
|
// Quantize the pixel
|
||||
|
pixelValue = this.QuantizePixel(new Color32(sourcePixel)); |
||||
|
|
||||
|
// And setup the previous pointer
|
||||
|
previousPixel = sourcePixel; |
||||
|
} |
||||
|
|
||||
|
// And set the pixel in the output
|
||||
|
Marshal.WriteByte(destinationPixel, pixelValue); |
||||
|
|
||||
|
sourcePixel = (IntPtr)((long)sourcePixel + this.pixelSize); |
||||
|
destinationPixel = (IntPtr)((long)destinationPixel + 1); |
||||
|
} |
||||
|
|
||||
|
// Add the stride to the source row
|
||||
|
sourceRow = (IntPtr)((long)sourceRow + sourceData.Stride); |
||||
|
|
||||
|
// And to the destination row
|
||||
|
destinationRow = (IntPtr)((long)destinationRow + outputData.Stride); |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
// Ensure that I unlock the output bits
|
||||
|
output.UnlockBits(outputData); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Override this to process the pixel in the first pass of the algorithm
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">The pixel to quantize</param>
|
||||
|
/// <remarks>
|
||||
|
/// This function need only be overridden if your quantize algorithm needs two passes,
|
||||
|
/// such as an Octree quantizer.
|
||||
|
/// </remarks>
|
||||
|
protected virtual void InitialQuantizePixel(Color32 pixel) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Override this to process the pixel in the second pass of the algorithm
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixel">The pixel to quantize</param>
|
||||
|
/// <returns>The quantized value</returns>
|
||||
|
protected abstract byte QuantizePixel(Color32 pixel); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Retrieve the palette for the quantized image
|
||||
|
/// </summary>
|
||||
|
/// <param name="original">Any old palette, this is overwritten</param>
|
||||
|
/// <returns>The new color palette</returns>
|
||||
|
protected abstract ColorPalette GetPalette(ColorPalette original); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Structure that defines a 32 bit color
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// This structure is used to read data from a 32 bits per pixel image
|
||||
|
/// in memory, and is ordered in this manner as this is the way that
|
||||
|
/// the data is laid out in memory
|
||||
|
/// </remarks>
|
||||
|
[StructLayout(LayoutKind.Explicit)] |
||||
|
public struct Color32 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Holds the blue component of the colour
|
||||
|
/// </summary>
|
||||
|
[FieldOffset(0)] |
||||
|
private byte blue; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Holds the green component of the colour
|
||||
|
/// </summary>
|
||||
|
[FieldOffset(1)] |
||||
|
private byte green; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Holds the red component of the colour
|
||||
|
/// </summary>
|
||||
|
[FieldOffset(2)] |
||||
|
private byte red; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Holds the alpha component of the colour
|
||||
|
/// </summary>
|
||||
|
[FieldOffset(3)] |
||||
|
private byte alpha; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Permits the color32 to be treated as a 32 bit integer.
|
||||
|
/// </summary>
|
||||
|
[FieldOffset(0)] |
||||
|
private int argb; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="T:ImageProcessor.Imaging.Quantizer.Color32">Color32</see> structure.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sourcePixel">The pointer to the pixel.</param>
|
||||
|
public Color32(IntPtr sourcePixel) |
||||
|
{ |
||||
|
this = (Color32)Marshal.PtrToStructure(sourcePixel, typeof(Color32)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the blue component of the colour
|
||||
|
/// </summary>
|
||||
|
public byte Blue |
||||
|
{ |
||||
|
get { return this.blue; } |
||||
|
set { this.blue = value; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the green component of the colour
|
||||
|
/// </summary>
|
||||
|
public byte Green |
||||
|
{ |
||||
|
get { return this.green; } |
||||
|
set { this.green = value; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the red component of the colour
|
||||
|
/// </summary>
|
||||
|
public byte Red |
||||
|
{ |
||||
|
get { return this.red; } |
||||
|
set { this.red = value; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the alpha component of the colour
|
||||
|
/// </summary>
|
||||
|
public byte Alpha |
||||
|
{ |
||||
|
get { return this.alpha; } |
||||
|
set { this.alpha = value; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ARGB component, permitting the color32 to be treated as a 32 bit integer.
|
||||
|
/// </summary>
|
||||
|
public int Argb |
||||
|
{ |
||||
|
get { return this.argb; } |
||||
|
set { this.argb = value; } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the color for this Color32 object
|
||||
|
/// </summary>
|
||||
|
public Color Color |
||||
|
{ |
||||
|
get { return Color.FromArgb(this.Alpha, this.Red, this.Green, this.Blue); } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue