private Bitmap GenerateBitmap(Bitmap bitmap, IDitherer ditherer) { Rectangle bounds = Rectangle.FromLTRB(0, 0, bitmap.Width, bitmap.Height); // Lock source bits for reading BitmapData sourceData = bitmap.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); // Lock target bits for writing Bitmap canvas = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format8bppIndexed); BitmapData targetData = canvas.LockBits(bounds, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); // Copy the palette to the target bitmap CopyPalette(canvas); try { StartWritingProcess(bitmap.Height, bitmap.Width, sourceData.Scan0.ToInt64(), targetData.Scan0.ToInt64(), sourceData.Stride, targetData.Stride, ditherer); } finally { bitmap.UnlockBits(sourceData); canvas.UnlockBits(targetData); } return(canvas); }
public void DitherPerformanceTest(bool errorDiffusion) { using var bmpRef = Icons.Information.ExtractBitmap(new Size(256, 256)); IQuantizer quantizer = PredefinedColorsQuantizer.SystemDefault8BppPalette(); IDitherer ditherer = errorDiffusion ? (IDitherer)ErrorDiffusionDitherer.FloydSteinberg : OrderedDitherer.Bayer8x8; new PerformanceTest { TestName = $"{bmpRef.Width}x{bmpRef.Height}@{bmpRef.GetColorCount()} {(errorDiffusion ? "Error Diffusion" : "Ordered")}", Iterations = 100, CpuAffinity = null } .AddCase(() => { using var result = bmpRef.CloneBitmap(); result.Dither(quantizer, ditherer); }, "BitmapExtensions.Dither") .AddCase(() => { using var result = bmpRef.CloneBitmap(); using (IBitmapDataInternal bitmapData = BitmapDataFactory.CreateBitmapData(result, ImageLockMode.ReadWrite)) using (IQuantizingSession quantizingSession = quantizer.Initialize(bitmapData)) using (IDitheringSession ditheringSession = ditherer.Initialize(bitmapData, quantizingSession)) { var row = bitmapData.DoGetRow(0); int width = bitmapData.Width; do { for (int x = 0; x < width; x++) { row.DoSetColor32(x, ditheringSession.GetDitheredColor(row.DoGetColor32(x), x, row.Index)); } } while (row.MoveNextRow()); } }, "Sequential dithering") .DoTest() .DumpResults(Console.Out); }
internal void PerformDraw(IQuantizer quantizer, IDitherer ditherer) { if (quantizer == null) { PerformDrawDirect(); return; } IReadableBitmapData initSource = SourceRectangle.Size == Source.GetSize() ? Source : Source.Clip(SourceRectangle); try { Debug.Assert(!quantizer.InitializeReliesOnContent || !Source.HasMultiLevelAlpha(), "This draw performs blending on-the-fly but the used quantizer would require two-pass processing"); context.Progress?.New(DrawingOperation.InitializingQuantizer); using (IQuantizingSession quantizingSession = quantizer.Initialize(initSource, context)) { if (context.IsCancellationRequested) { return; } if (quantizingSession == null) { throw new InvalidOperationException(Res.ImagingQuantizerInitializeNull); } // quantizing without dithering if (ditherer == null) { PerformDrawWithQuantizer(quantizingSession); return; } // quantizing with dithering Debug.Assert(!ditherer.InitializeReliesOnContent || !Source.HasMultiLevelAlpha(), "This draw performs blending on-the-fly but the used ditherer would require two-pass processing"); context.Progress?.New(DrawingOperation.InitializingDitherer); using IDitheringSession ditheringSession = ditherer.Initialize(initSource, quantizingSession, context); if (context.IsCancellationRequested) { return; } if (ditheringSession == null) { throw new InvalidOperationException(Res.ImagingDithererInitializeNull); } PerformDrawWithDithering(quantizingSession, ditheringSession); } } finally { if (!ReferenceEquals(initSource, Source)) { initSource.Dispose(); } } }
/// <summary> /// Returns an array of bytes to be sent to the /dev/usb/lp0 device. Will rotate and scale the image before dithering. /// </summary> /// <param name="inputImage"></param> /// <param name="ditherer">The ditherer to be used. Use new BurkesDitherer() if unsure.</param> /// <param name="rotateForLargerPrint">If true, the image will be rotated 90 degrees if that would increase the printed size.</param> /// <returns></returns> public byte[] ImageToPrintCommands(Bitmap inputImage, IDitherer ditherer, bool rotateForLargerPrint = true) { var resized = ScaleToFitPage(inputImage, rotateForLargerPrint); BWImage result = ditherer.GetBWImage(resized); byte[] printCommands = RasterToPrintCommands(result); return(printCommands); }
/// <summary> /// Draws a bitmap while applying dithering and/or quantization /// </summary> /// <returns>the altered bitmap</returns> public override Bitmap Draw() { if (!imageStore.QuantizerReady) { throw new Exception("The Quantizer was not ready yet."); } TotalError = 0; IDitherer ditherer = imageStore.Ditherer; Bitmap canvas; BitmapData sourceData; BitmapData targetData; int width; int height; lock (imageStore.Image) { width = imageStore.Image.Width; height = imageStore.Image.Height; Rectangle bounds = Rectangle.FromLTRB(0, 0, imageStore.Image.Width, imageStore.Image.Height); // Lock source bits for reading sourceData = imageStore.Image.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); // Lock target bits for writing canvas = new Bitmap(imageStore.Image.Width, imageStore.Image.Height, PixelFormat.Format8bppIndexed); targetData = canvas.LockBits(bounds, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); } // Copy the palette to the target bitmap CopyPalette(canvas); try { long total = width * height; Models.Color[] ditherDistortion = new Models.Color[total]; LoopRows(width, height, sourceData, targetData, ditherDistortion, ditherer); AverageError = TotalError / total; } finally { lock (imageStore.Image) { imageStore.Image.UnlockBits(sourceData); } canvas.UnlockBits(targetData); } return(canvas); }
/// <summary> /// Draws a bitmap while applying dithering and/or quantization /// </summary> /// <returns>the altered bitmap</returns> public override Bitmap Draw() { if (!imageStore.QuantizerReady) { throw new Exception("The Quantizer was not ready yet."); } TotalError = 0; IDitherer ditherer = imageStore.Ditherer; // Lock the image for thread-safeness lock (imageStore.Image) { return(GenerateBitmap(imageStore.Image, ditherer)); } }
private void ButtonLoadImage_Click(object sender, EventArgs e) { if (OpenFileDialogImageLoader.ShowDialog() == DialogResult.OK) { ButtonSaveImage.Enabled = false; Quantizer quantizer = GetQuantizer(); IDitherer ditherer = GetDitherer(); ProgressBarQuantization.Value = 0; ImagePath = OpenFileDialogImageLoader.FileName; LabelPath.Text = ImagePath; var image = new Bitmap(ImagePath); PictureBoxLoadedImage.Image = image; imageStore = new ImageStore(image, quantizer, ditherer); drawer = GetDrawer(imageStore); imageStore.InitFinished += AfterInit; drawer.ProgressUpdate += ProgressUpdate; } }
/// <summary> /// Generate a task that writes to the target bitmap /// </summary> /// <param name="sourceOffset">the offset to the source bitmap in memory</param> /// <param name="targetOffset">the offset to the target bitmap in memory</param> /// <param name="width">the width of the image</param> /// <param name="height">the height of the image</param> /// <param name="row">the row this task has to process</param> /// <param name="behind">the behind constant of the ditherer</param> /// <param name="progress">the progress array, this will get updated as the task progresses, and is used for checking if this task can continue</param> /// <param name="ditherDistortion">the dither distortion overlay, this will be edited by this task</param> /// <param name="ditherer">the ditherer to be used</param> /// <returns>the task</returns> private Task GenerateTask(long sourceOffset, long targetOffset, int width, int height, int row, int behind, int[] progress, Models.Color[] ditherDistortion, IDitherer ditherer) { // Re initialize the source and target rows byte[] targetLine = new byte[width]; int[] sourceLine = new int[width]; var id = row; Task t = new Task(() => { Marshal.Copy(new IntPtr(sourceOffset), sourceLine, 0, width); for (int index = 0; index < width; index++) { if (id != 0) { // Check if the task processing the row above ours is done dithering the pixel this task is processing while (index > (progress[id - 1] - behind)) { System.Threading.Thread.Sleep(1); } } // Read the color from the source image Color color = Color.FromArgb(sourceLine[index]); // Add the dithering to the pixel var colorAsColor = new Models.Color(color) + ditherDistortion[index + id * width]; // Get the index of the color closest to the dithered pixel targetLine[index] = (byte)imageStore.Quantizer.GetPaletteIndex(colorAsColor); // Get the distance to dither the other pixels! var distance = System.Math.Sqrt(Util.Math.Distance(new Models.Color(color), imageStore.Quantizer.GetColorByIndex(targetLine[index]))); ditherer.Dither(colorAsColor, imageStore.Quantizer.GetColorByIndex(targetLine[index]), ditherDistortion, index, id, width, height); TotalError += distance; // Free up memory by deleting this entry in the dither distortion matrix ditherDistortion[index + id * width] = null; progress[id]++; } // Increase this significantly so task below this can get to the end progress[id] += width; Marshal.Copy(targetLine, 0, new IntPtr(targetOffset), width); // Update progress completed++; lock (ProgressUpdate) { ProgressUpdate?.Invoke(this, new ProgressEventArgs((int)(completed / (float)height * 100))); } }); return(t); }
/// <summary> /// Spawns tasks that will each write a line to the target bitmap /// </summary> /// <param name="height">the height of the bitmap</param> /// <param name="width">the width of the bitmap</param> /// <param name="sourceOffset">the offset of the source bitmap in memory</param> /// <param name="targetOffset">the offset of the target bitmap in memory</param> /// <param name="sourceStride">the stride of the source bitmap in memory</param> /// <param name="targetStride">the stride of the trarget bitmap in memory</param> /// <param name="ditherer">the ditherer to be used</param> /// <param name="behind">how many pixels to the left of the currently processed pixel the ditherer writes distortion to</param> /// <returns>A list of writing tasks</returns> private Task[] GetTasks(int height, int width, long sourceOffset, long targetOffset, long sourceStride, int targetStride, IDitherer ditherer, int behind) { Models.Color[] ditherDistortion = new Models.Color[width * height]; // Keeps track of the progress of every task, so they can know when to start // since they have to leave enough time so they won't quantize before dithering is finished // this is what behind is used for int[] progress = new int[height]; Task[] tasks = new Task[height]; completed = 0; for (int row = 0; row < height; row++) { Task t = GenerateTask(sourceOffset, targetOffset, width, height, row, behind, progress, ditherDistortion, ditherer); tasks[row] = t; // Set ConfigureAwait to false, to avoid deadlocking t.ConfigureAwait(false); t.Start(); sourceOffset += sourceStride; targetOffset += targetStride; } return(tasks); }
public void ConvertPixelFormatCustomTest(string testName, PixelFormat pixelFormat, IQuantizer quantizer, IDitherer ditherer) { if (!pixelFormat.IsSupportedNatively()) { Assert.Inconclusive($"Pixel format is not supported: {pixelFormat}"); } using var source = Icons.Information.ExtractBitmap(new Size(256, 256)); using var converted = source.ConvertPixelFormat(pixelFormat, quantizer, ditherer); Assert.AreEqual(pixelFormat, converted.PixelFormat); SaveImage(testName, converted); }
public Quantizer(Bitmap image, IDitherer ditherer) { this.image = image; this.ditherer = ditherer; }
/// <summary> /// Clears the content of the specified <paramref name="bitmapData"/> and fills it with the specified <paramref name="color"/>. /// <br/>This method is similar to <see cref="Graphics.Clear">Graphics.Clear</see> except that this one supports any <see cref="PixelFormat"/> and also dithering. /// </summary> /// <param name="bitmapData">The <see cref="IWritableBitmapData"/> to be cleared.</param> /// <param name="color">A <see cref="Color32"/> that represents the desired result color of the <paramref name="bitmapData"/>. /// If it has transparency, which is not supported by <see cref="IBitmapData.PixelFormat"/> of <paramref name="bitmapData"/>, then the result might be either /// completely transparent (depends also on <see cref="IBitmapData.AlphaThreshold"/>), or the color will be blended with <see cref="IBitmapData.BackColor"/>. /// </param> /// <param name="ditherer">The ditherer to be used for the clearing. Has no effect if <see cref="IBitmapData.PixelFormat"/> of <paramref name="bitmapData"/> has at least 24 bits-per-pixel size. This parameter is optional. /// <br/>Default value: <see langword="null"/>.</param> /// <remarks> /// <note>This method adjusts the degree of parallelization automatically, blocks the caller, and does not support cancellation or reporting progress. Use the <see cref="BeginClear">BeginClear</see> /// or <see cref="ClearAsync">ClearAsync</see> (in .NET 4.0 and above) methods for asynchronous call and to adjust parallelization, set up cancellation and for reporting progress.</note> /// </remarks> /// <seealso cref="BitmapExtensions.Clear(Bitmap, Color, IDitherer, Color, byte)"/> public static void Clear(this IWritableBitmapData bitmapData, Color32 color, IDitherer ditherer = null) { if (bitmapData == null) throw new ArgumentNullException(nameof(bitmapData), PublicResources.ArgumentNull); DoClear(AsyncContext.Null, bitmapData, color, ditherer); }
/// <summary> /// Process a line from the bitmap image /// </summary> /// <param name="width">the width of the image</param> /// <param name="height">the height of the image</param> /// <param name="row">the current row being processed</param> /// <param name="ditherDistortion">the dither distortion overlay</param> /// <param name="sourceLine">the source row of pixels</param> /// <param name="targetLine">the target row of pixels</param> /// <param name="ditherer">the ditherer to use</param> private void GenerateLine(int width, int height, int row, Models.Color[] ditherDistortion, int[] sourceLine, byte[] targetLine, IDitherer ditherer) { for (int index = 0; index < width; index++) { // Read the color from the source image Color color = Color.FromArgb(sourceLine[index]); // Add the dithering to the pixel var colorAsColor = new Models.Color(color) + ditherDistortion[index + row * width]; // Get the index of the color closest to the dithered pixel targetLine[index] = (byte)imageStore.Quantizer.GetPaletteIndex(colorAsColor); // Get the distance to dither the other pixels! var distance = System.Math.Sqrt(Util.Math.Distance(new Models.Color(color), imageStore.Quantizer.GetColorByIndex(targetLine[index]))); ditherer.Dither(colorAsColor, imageStore.Quantizer.GetColorByIndex(targetLine[index]), ditherDistortion, index, row, width, height); TotalError += distance; } }
/// <summary> /// Creates tasks for writing to pixels and calculates the average error /// </summary> /// <param name="height">the height of the bitmap</param> /// <param name="width">the width of the bitmap</param> /// <param name="sourceOffset">the offset of the source bitmap in memory</param> /// <param name="targetOffset">the offset of the target bitmap in memory</param> /// <param name="sourceStride">the stride of the source bitmap in memory</param> /// <param name="targetStride">the stride of the trarget bitmap in memory</param> /// <param name="ditherer">the ditherer to be used</param> private void StartWritingProcess(int height, int width, long sourceOffset, long targetOffset, int sourceStride, int targetStride, IDitherer ditherer) { int behind = imageStore.Ditherer.GetBehind() + 1; var tasks = GetTasks(height, width, sourceOffset, targetOffset, sourceStride, targetStride, ditherer, behind); Task.WaitAll(tasks); lock (ProgressUpdate) { ProgressUpdate?.Invoke(this, new ProgressEventArgs(100)); } AverageError = TotalError / (width * height); }
/// <summary> /// Constructor /// </summary> /// <param name="image">the image this class is storing</param> /// <param name="quantizer">the quantizer that will be used</param> /// <param name="ditherer">the ditherer that will be used</param> public ImageStore(Bitmap image, IQuantizer quantizer, IDitherer ditherer) { Image = Cloner.DeepClone(image); Quantizer = quantizer; Ditherer = ditherer; }
internal GenerateTask(PixelFormat pixelFormat, IQuantizer quantizer, IDitherer ditherer) { PixelFormat = pixelFormat; Quantizer = quantizer; Ditherer = ditherer; }
/// <summary> /// Begins to clear the content of the specified <paramref name="bitmapData"/> and fills it with the specified <paramref name="color"/> asynchronously. /// <br/>This method is similar to <see cref="Graphics.Clear">Graphics.Clear</see> except that this one supports any <see cref="PixelFormat"/> and also dithering. /// </summary> /// <param name="bitmapData">The <see cref="IWritableBitmapData"/> to be cleared.</param> /// <param name="color">A <see cref="Color32"/> that represents the desired result color of the <paramref name="bitmapData"/>. /// If it has transparency, which is not supported by <see cref="IBitmapData.PixelFormat"/> of <paramref name="bitmapData"/>, then the result might be either /// completely transparent (depends also on <see cref="IBitmapData.AlphaThreshold"/>), or the color will be blended with <see cref="IBitmapData.BackColor"/>. /// </param> /// <param name="ditherer">The ditherer to be used for the clearing. Has no effect if <see cref="IBitmapData.PixelFormat"/> of <paramref name="bitmapData"/> has at least 24 bits-per-pixel size. This parameter is optional. /// <br/>Default value: <see langword="null"/>.</param> /// <param name="asyncConfig">The configuration of the asynchronous operation such as parallelization, cancellation, reporting progress, etc. This parameter is optional. /// <br/>Default value: <see langword="null"/>.</param> /// <returns>A <see cref="Task"/> that represents the asynchronous operation, which could still be pending.</returns> /// <remarks> /// <para>This method is not a blocking call even if the <see cref="AsyncConfigBase.MaxDegreeOfParallelism"/> property of the <paramref name="asyncConfig"/> parameter is 1.</para> /// </remarks> public static Task ClearAsync(this IWritableBitmapData bitmapData, Color32 color, IDitherer ditherer = null, TaskConfig asyncConfig = null) { if (bitmapData == null) throw new ArgumentNullException(nameof(bitmapData), PublicResources.ArgumentNull); return AsyncContext.DoOperationAsync(ctx => DoClear(ctx, bitmapData, color, ditherer), asyncConfig); }
private static void DoClear(IAsyncContext context, IWritableBitmapData bitmapData, Color32 color, IDitherer ditherer) { IBitmapDataInternal accessor = bitmapData as IBitmapDataInternal ?? new BitmapDataWrapper(bitmapData, false, true); try { if (ditherer == null || !accessor.PixelFormat.CanBeDithered()) ClearDirect(context, accessor, color); else ClearWithDithering(context, accessor, color, ditherer); } finally { if (!ReferenceEquals(accessor, bitmapData)) accessor.Dispose(); } }
private static void AdjustQuantizerAndDitherer(IBitmapData target, ref IQuantizer quantizer, ref IDitherer ditherer) { if (quantizer != null || ditherer == null) { return; } if (target.PixelFormat.CanBeDithered()) { quantizer = PredefinedColorsQuantizer.FromBitmapData(target); } else { ditherer = null; } }
/// <summary> /// Loop over all bitmap rows and replace them with a palette color /// </summary> /// <param name="width">the width of the bitmap</param> /// <param name="height">the height of the bitmap</param> /// <param name="sourceData">the source bitmap data</param> /// <param name="targetData">the target bitmap data</param> /// <param name="ditherDistortion">the dither distortion overlay</param> /// <param name="ditherer">the ditherer to use</param> private void LoopRows(int width, int height, BitmapData sourceData, BitmapData targetData, Models.Color[] ditherDistortion, IDitherer ditherer) { int completed = 0; long sourceOffset = sourceData.Scan0.ToInt64(); long targetOffset = targetData.Scan0.ToInt64(); for (int row = 0; row < height; row++) { // Re initialize the source and target rows byte[] targetLine = new byte[width]; int[] sourceLine = new int[width]; Marshal.Copy(new IntPtr(sourceOffset), sourceLine, 0, width); GenerateLine(width, height, row, ditherDistortion, sourceLine, targetLine, ditherer); Marshal.Copy(targetLine, 0, new IntPtr(targetOffset), width); sourceOffset += sourceData.Stride; targetOffset += targetData.Stride; // Update progress completed++; ProgressUpdate?.Invoke(this, new ProgressEventArgs((int)(completed / (float)height * 100))); } }