using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Windows.Forms; using SimplePaletteQuantizer.ColorCaches; using SimplePaletteQuantizer.ColorCaches.Common; using SimplePaletteQuantizer.ColorCaches.Octree; using SimplePaletteQuantizer.Extensions; using SimplePaletteQuantizer.Helpers; using SimplePaletteQuantizer.Quantizers; using SimplePaletteQuantizer.Quantizers.MedianCut; /**********************CartoonApp************************ ' ' Code Explication: Working with CartoonApp Code ' ' Fields: contains variables for both Sobel and Palette Quantization; ' stored as global variables, i.e. no parameter passing required (dangerous, though) ' '********************************************************************/ namespace SimplePaletteQuantizer { public partial class CartoonApp : Form { #region | Fields | //Quantize Fields private Image gifImage; private Image sourceImage; private Boolean turnOnEvents; private Int32 projectedGifSize; private FileInfo sourceFileInfo; private Image targetImage; //contains image selected by user private IColorCache activeColorCache = new OctreeColorCache(); private IColorQuantizer activeQuantizer = new MedianCutQuantizer(); private Dictionary errorCache; //Sobel Fields private int[,] xmask = new int[3, 3]; //horizontal sobel mask private int[,] ymask = new int[3, 3]; //vertical sobel mask private int WIDTH, HEIGHT; //dimensions of phot private int[,] BWArray; //stores pixels of Black&White photo private int[,] SobelData; //stores pixels of Sobel edges private int sliderval = 190; //sensitivity of Sobel edge detection (default: 190; range: 0-255) private Image SensitivityChange; //image used when 'sliderval' is moved #endregion #region | Constructors | /// /// Initializes a new instance of the class. /// public CartoonApp() { InitializeComponent(); } #endregion #region | Update methods | /**********************Update Methods************************* ' ' purpose: Obtain image and apply palette simplification algorithm ' ' '********************************************************************/ private void UpdateImages() { // prepares quantizer errorCache.Clear(); // updates source image UpdateSourceImage(); // tries to retrieve an image based on HSB quantization try { DateTime before = DateTime.Now; targetImage = GetQuantizedImage(sourceImage); SensitivityChange = targetImage; //CALL TO SOBEL CODE/////////////////////// targetImage = Sobel_Process(targetImage);// /////////////////////////////////////////// TimeSpan duration = DateTime.Now - before; TimeSpan perPixel = new TimeSpan(duration.Ticks/(sourceImage.Width*sourceImage.Height)); pictureTarget.Image = targetImage; // new GIF and PNG sizes Int32 newGifSize, newPngSize; // retrieves a GIF image based on our HSB-quantized one GetConvertedImage(targetImage, ImageFormat.Gif, out newGifSize); // retrieves a PNG image based on our HSB-quantized one GetConvertedImage(targetImage, ImageFormat.Png, out newPngSize); } catch (NotSupportedException ex) { MessageBox.Show(ex.Message); } } private void UpdateSourceImage() { pictureSource.Image = sourceImage; } #endregion #region | Functions | private static PixelFormat GetFormatByColorCount(Int32 colorCount) { if (colorCount <= 0 || colorCount > 256) { String message = string.Format("A color count '{0}' not supported!", colorCount); throw new NotSupportedException(message); } PixelFormat result = PixelFormat.Format1bppIndexed; if (colorCount > 16) { result = PixelFormat.Format8bppIndexed; } else if (colorCount > 2) { result = PixelFormat.Format4bppIndexed; } return result; } private Int32 GetColorCount() //receives color count from trackbar (default: 16) { switch (colorcount.Value) { case 7: colortext.Text = "2 colors"; return 2; case 6: colortext.Text = "4 colors"; return 4; case 5: colortext.Text = "8 colors"; return 8; case 4: colortext.Text = "16 colors"; return 16; case 3: colortext.Text = "32 colors"; return 32; case 2: colortext.Text = "64 colors"; return 64; case 1: colortext.Text = "128 colors"; return 128; case 0: colortext.Text = "256 colors"; return 256; default: throw new NotSupportedException("Only 2, 4, 8, 16, 32, 64, 128 and 256 colors are supported."); } } #endregion #region | Methods | private void ChangeColorCache() { // applies current UI selection if (activeQuantizer is BaseColorQuantizer) { BaseColorQuantizer quantizer = (BaseColorQuantizer) activeQuantizer; quantizer.ChangeCacheProvider(activeColorCache); } } private void GenerateProjectedGif() { // retrieves a projected GIF image (automatic C# conversion) Int32 projectedSize; gifImage = GetConvertedImage(sourceImage, ImageFormat.Gif, out projectedSize); projectedGifSize = projectedSize; } private static Image GetConvertedImage(Image image, ImageFormat newFormat, out Int32 imageSize) { Image result; // saves the image to the stream, and then reloads it as a new image format; thus conversion.. kind of using (MemoryStream stream = new MemoryStream()) { image.Save(stream, newFormat); stream.Seek(0, SeekOrigin.Begin); imageSize = (Int32)stream.Length; result = Image.FromStream(stream); } return result; } private Image GetQuantizedImage(Image image) { // checks whether a source image is valid if (image == null) { const String message = "Cannot quantize a null image."; throw new ArgumentNullException(message); } // scans the image for pixel colors, and adds them to a quantizer Int32 colorCount = GetColorCount(); activeQuantizer.Prepare(sourceImage); image.AddColorsToQuantizer(activeQuantizer); Int32 originalColorCount = activeQuantizer.GetColorCount(); // creates a target bitmap in 8-bit format Boolean isSourceIndexed = image.PixelFormat.IsIndexed(); PixelFormat targetPixelFormat = GetFormatByColorCount(colorCount); Bitmap result = new Bitmap(image.Width, image.Height, targetPixelFormat); Int64 imageColorError = 0; List sourcePalette = isSourceIndexed ? image.GetPalette() : null; List palette = activeQuantizer.GetPalette(colorCount); result.SetPalette(palette); // moves to next pixel for both images Action quantization = (sourcePixel, targetPixel) => { Color color = isSourceIndexed ? sourcePalette[sourcePixel.GetIndex()] : sourcePixel.Color; color = QuantizationHelper.ConvertAlpha(color); Int32 paletteIndex = activeQuantizer.GetPaletteIndex(color); targetPixel.SetIndex((Byte)paletteIndex); // if turned on, it calculates root mean square deviation Int64 pixelError; if (!errorCache.TryGetValue(color, out pixelError)) { pixelError = ColorModelHelper.GetColorEuclideanDistance(ColorModel.RedGreenBlue, color, palette[paletteIndex]); errorCache[color] = pixelError; } imageColorError += pixelError; }; // processes the quantization image.ProcessImagePixels(result, quantization); // calculates root mean square deviation Double nrmsd = false ? 0.0 : Math.Sqrt(imageColorError / (3.0 * result.Width * result.Height)) / 255.0; String nrmsdString = true ? string.Format(" (NRMSD = {0:0.#####})", nrmsd) : string.Empty; // returns the quantized image return result; } #endregion #region << Events >> private void MainFormLoad(object sender, EventArgs e) { errorCache = new Dictionary(); turnOnEvents = false; ChangeColorCache(); turnOnEvents = true; } private void MainFormResize(object sender, EventArgs e) { panelRight.Width = panelMain.Width / 2; } private void ButtonBrowseClick(object sender, EventArgs e) { if (dialogOpenFile.ShowDialog() == DialogResult.OK) { editFilename.Text = Path.GetFileName(dialogOpenFile.FileName); sourceFileInfo = new FileInfo(dialogOpenFile.FileName); sourceImage = Image.FromFile(dialogOpenFile.FileName); colorcount.Enabled = true; trackbar.Enabled = true; savebutton.Enabled = true; GenerateProjectedGif(); UpdateImages(); } } private void ListSourceSelectedIndexChanged(object sender, EventArgs e) { if (turnOnEvents) UpdateSourceImage(); } #endregion #region | Sobel | private Image Sobel_Process(Image img) { //Variables Initialization WIDTH = img.Width; HEIGHT = img.Height; BWArray = new int[WIDTH, HEIGHT]; //array that contains grayscale pixels SobelData = new int[WIDTH, HEIGHT]; //array that contains outlined photo Initmask(); Bitmap grayed = ToGrayscale(img); //return a B&W image of quantized image InitializeSolidBW(BWArray, grayed); //store grayscale image into BWArray Sobel(SobelData, BWArray); //alters SobelData so as to contain bitmap array of sobel outline return Merge(SobelData, img); //returns concatenated image to calling function } /**********************Merge() subroutine************************* ' ' purpose: lays detected edges stored from Sobel over quantized image ' ' input: SobelData[][] and Image img ' ' output: returns a new file of Image type, the final product ' '********************************************************************/ public Image Merge(int[,] SobelData, Image img) { Bitmap bitmap = new Bitmap(img); Bitmap result = new Bitmap(bitmap.Width, bitmap.Height); for (int i = 0; i < bitmap.Width; i++) { for (int j = 0; j < bitmap.Height; j++) { if (SobelData[i,j] == 255) { Color NotBlack = bitmap.GetPixel(i,j); result.SetPixel(i, j, NotBlack); } else { int Line = SobelData[i,j]; Color Black = Color.FromArgb(Line, Line, Line); result.SetPixel(i, j, Black); } } } Image result_Image = (Image)result; return result_Image; } /**********************Sobel() subroutine************************* ' ' purpose: using grayscale version of image, creates and stores ' edges in SobelData array ' ' input: SobelData[][] and BWArray[][] ' ' output: updated SobelData array ' '********************************************************************/ public void Sobel(int[,] SobelData, int[,] BWArray) { int sumx, sumy, sum; //sum based on xmask weights //sum based on ymask weights //overall absolute value of combined weights-->is the final sobel data for (int x1 = 0; x1 < HEIGHT; x1++) //fill in rows 120 to 1 COULD BE < (ml) { for (int y1 = 0; y1 < WIDTH; y1++) //fill in columns 120 to 1 COULD BE < (ml) { sumx = 0; //reinitialize these to zero every time through the loop sumy = 0; SobelData[y1, x1] = 255; //initialize each element of the array (avoids separate initializing loop) //if not on the border of the image, step into the if statement if (y1 != 0 && y1 != (WIDTH - 1) && x1 != 0 && x1 != HEIGHT - 1) { for (int i = -1; i <= 1; i++) //work through the masks (MILES NOTE: MAYBE <1 (?) ) { for (int j = -1; j <= 1; j++) //move across columns first, then go to next row { //pic is stored (col, row) while the mask is (row,col) //x and i refer to rows, y and j refer to columns sumx += (BWArray[(y1 + j), (x1 + i)] * xmask[(i + 1), (j + 1)]); //switching to "i+1" because xmask/ymask arrays changed (ml) sumy += (BWArray[(y1 + j), (x1 + i)] * ymask[(i + 1), (j + 1)]); } } sum = Math.Abs(sumx) + Math.Abs(sumy); //the larger the overall absolute value, the greater the difference //between the adjacent pixels //adjust the meaning of "black" and "white" via the slider control on the form if (sum >= (255 - sliderval)) sum = 255; else if (sum <= sliderval) sum = 0; else sum = 0; SobelData[y1, x1] = 255 - sum; //subtract so edges are outlined in black, everything else is white } } } } /**********************initmask() subroutine************************* ' ' purpose: initialize the masks used by sobel() giving adjacent ' pixels the appropriate "weight" ' ' input: none ' ' output: two matrices defining the weights to be used on adjacent ' pixels when finding the sobel data ' ' note: increasing the values in the masks will increase the ' sensitivity of sobel...however, for the most part this ' simply means more "noise" rather than better defined edges ' also: the numbers have to stay relatively small because of the ' sobel computations-->the bigger the multiplier in the matrix ' the higher the results are on average, meaning more pixels ' are black more often (hence the noise issue) ' '********************************************************************/ public void Initmask() { xmask[0, 0] = -1; xmask[0, 1] = 0; xmask[0, 2] = 1; xmask[1, 0] = -2; xmask[1, 1] = 0; xmask[1, 2] = 2; xmask[2, 0] = -1; xmask[2, 1] = 0; xmask[2, 2] = 1; ymask[0, 0] = 1; ymask[0, 1] = 2; ymask[0, 2] = 1; ymask[1, 0] = 0; ymask[1, 1] = 0; ymask[1, 2] = 0; ymask[2, 0] = -1; ymask[2, 1] = -2; ymask[2, 2] = -1; } #endregion #region | Grayscale | /**********************InitializeSolidBW subroutine************************* ' ' purpose: stores bitmap image into BWArray for Sobel manipulation ' ' input: pixel_array, orig ' ' output: altered pixel_array (returns as BWArray) ' '********************************************************************/ public void InitializeSolidBW(int[,] pixel_array, Bitmap orig) { int R; for (int i = 0; i < WIDTH; i++) { for (int j = 0; j < HEIGHT; j++) { Color c1 = orig.GetPixel(i, j); R = c1.R; pixel_array[i, j] = R; } } } /**********************ToGrayscale subroutine************************* ' ' purpose: gets image, converts to grayscale, and returns new bitmap ' ' input: original img ' ' output: grayscaled image in bitmap format ' '********************************************************************/ public Bitmap ToGrayscale(Image img) { Bitmap bitmap = new Bitmap(img); Bitmap result = new Bitmap(bitmap.Width, bitmap.Height); for (int x = 0; x < bitmap.Width; x++) { for (int y = 0; y < bitmap.Height; y++) { var grayColor = ToGrayscaleColor(bitmap.GetPixel(x, y)); result.SetPixel(x, y, grayColor); } } return result; } public static Color ToGrayscaleColor(Color color) { var level = (byte)((color.R + color.G + color.B) / 3); var result = Color.FromArgb(level, level, level); return result; } #endregion #region | Testing | public Image Draw_Image(int[,] Orig) { Bitmap New_Image = new Bitmap(WIDTH, HEIGHT); int R, G, B; for (int i = 0; i < WIDTH; i++) { for (int j = 0; j < HEIGHT; j++) { R = G = B = Orig[i, j]; New_Image.SetPixel(i, j, Color.FromArgb(R, G, B)); } } Image img = (Image)New_Image; return img; } #endregion private void trackbar_Scroll(object sender, EventArgs e) { sliderval = trackbar.Value; pictureTarget.Image = Sobel_Process(SensitivityChange); } private void colorcount_ValueChanged(object sender, EventArgs e) { if (turnOnEvents) UpdateImages(); } private void savebutton_Click(object sender, EventArgs e) { targetImage.Save("myfile.png", ImageFormat.Png); Stream myStream; SaveFileDialog saveFileDialog1 = new SaveFileDialog(); saveFileDialog1.Filter = "Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|All files (*.*)|*.*"; saveFileDialog1.FilterIndex = 2; saveFileDialog1.RestoreDirectory = true; if (saveFileDialog1.ShowDialog() == DialogResult.OK) { if ((myStream = saveFileDialog1.OpenFile()) != null) { targetImage.Save(myStream, ImageFormat.Jpeg ); myStream.Close(); } } } } }