loupasc

Members
  • Content Count

    22
  • Joined

  • Last visited

  • Days Won

    1

loupasc last won the day on May 18

loupasc had the most liked content!

Community Reputation

18

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

  1. After checking FFT results I noticed diamond shaped filter was leaking. There were a bug in the coefficient creation. Here the fixed version v1.2. The results show a better retention of unwanted frequencies. Minor upgrade : The filter cutoff parameter has four states corresponding to the number of lobes of the Sinc functions (4-8-12-16). // Name: WaveToolBox // Submenu: Advanced // Author: Pascal Ollive // Title: Wave tool box // Version: 1.2 // Desc: Reconstruction filter tool box // Keywords: convolution|kernel|filter // URL: // Help: #region UICode RadioButtonControl kernelShape = 0; // Shape|Square|Disc|Diamond IntSliderControl cutoffFactor = 2; // [1,4] Cutoff sharpness IntSliderControl radius = 8; // [8,100] Radius RadioButtonControl borderHandling = 2; // Border handling|Uniform|Extend|Mirror IntSliderControl contrastSliderIndex = 5; // [0,10] Contrast #endregion private int kernelRadius = 8; private double[] kernelMatrix = null; private double kernelNorm = 1.0; private static byte to8bit(double x) { if (x < 0) { return 0; } if (x > 255) { return 255; } return (byte) Math.Round(x); } private static double lanczos(double x, int lobeCount) { if (x == 0.0) { return 1.0; } double xMultiplyPI = Math.PI * x; return lobeCount * Math.Sin(xMultiplyPI) * Math.Sin(xMultiplyPI / lobeCount) / (xMultiplyPI * xMultiplyPI); } private static double computeLanczosCoef(int i, int j, byte shape, int lobeCount, int radius) { // center shift int x = i - radius + 1; int y = j - radius + 1; if (shape == 0) { // Square // normalize into lanczos window return lanczos((double) lobeCount * x / radius, lobeCount) * lanczos((double) lobeCount * y / radius, lobeCount); } if (shape == 1) { // Disc double d = Math.Sqrt(x * x + y * y); if (d < radius) { // normalize into lanczos window return lanczos(lobeCount * d / radius, lobeCount); } } if (shape == 2) { // Diamond (45 degrees rotated square and SQRT_2-rescaled) if (x < 0) { x = -x; } if (y < 0) { y = -y; } double p = (x + y); if (p < radius) { // normalize into lanczos window return lanczos(lobeCount * p / radius, lobeCount) * lanczos(lobeCount * (p - 2 * y) / radius, lobeCount); } } return 0.0; } private static double[] createLanczosKernel(byte shape, int lobeCount, int radius) { int dimension = 2 * radius - 1; double[] matrix = new double[dimension * dimension]; for (int j = 0; j < dimension; j++) { for (int i = 0; i < dimension; i++) { matrix[dimension * j + i] = computeLanczosCoef(i, j, shape, lobeCount, radius); } } return matrix; } private static double getCoefficientSum(double[] matrix) { double result = matrix[0]; for (int i = 1; i < matrix.Length; i++) { result = result + matrix[i]; } return result; } private static int surfaceExtend(int x, int maxValue) { if (x < 0) { return 0; } if (x > maxValue) { return maxValue; } return x; } private static int surfaceMirror(int x, int maxValue) { if (x < 0) { return - x; } if (x > maxValue) { return 2 * maxValue - x; } return x; } private ColorBgra getSafeSurfacePixel(Surface s, int x, int y) { if ((x >= 0) && (y >= 0) && (x < s.Width) && (y < s.Height)) { return s[x, y]; } if (borderHandling > 0) { if (borderHandling == 1) { // Extend return s[surfaceExtend(x, s.Width - 1), surfaceExtend(y, s.Height - 1)]; } // Mirror return s[surfaceMirror(x, s.Width - 1), surfaceMirror(y, s.Height - 1)]; } // Background color return ColorBgra.Black; } private double[] convolve(Surface s, int x, int y, double[] m, int radius) { double[] bgr = new double[3]; int dimension = 2 * radius - 1; int offset = 0; for (int j = 1; j <= dimension; j++) { for (int i = 1; i <= dimension; i++) { ColorBgra c = getSafeSurfacePixel(s, x + i - radius, y + j - radius); bgr[0] = bgr[0] + c.B * m[offset]; bgr[1] = bgr[1] + c.G * m[offset]; bgr[2] = bgr[2] + c.R * m[offset]; offset = offset + 1; } } return bgr; } private double applyContrast(double x) { return (256 + 10 * (contrastSliderIndex - 5)) * x / 256 - (5 * (contrastSliderIndex - 5)); } void PreRender(Surface dst, Surface src) { // Radius displayed by the slider is actually corresponding lanczos8 kernel radius kernelRadius = (radius * cutoffFactor) / 2; kernelMatrix = createLanczosKernel(kernelShape, 4 * cutoffFactor, kernelRadius); kernelNorm = getCoefficientSum(kernelMatrix); } void Render(Surface dst, Surface src, Rectangle rect) { for (int y = rect.Top; y < rect.Bottom; y++) { if (IsCancelRequested) return; for (int x = rect.Left; x < rect.Right; x++) { double[] bgr = convolve(src, x, y, kernelMatrix, kernelRadius); dst[x,y] = ColorBgra.FromBgr(to8bit(applyContrast(bgr[0] / kernelNorm)), to8bit(applyContrast(bgr[1] / kernelNorm)), to8bit(applyContrast(bgr[2] / kernelNorm))); } } } WaveToolBox.zip
  2. Hi Reptillian, I used ImageMagick to make the spectrum images which are created using FFT. My plugin uses convolution method (Sinc filters). FFT with spectrum mask is a powerful tool and versatile but I think it's hard to get into that. My goal is make a toolkit allowing image processing like FFT but lighter in term of computation resources and easier to use. (Goal is today far from filled )
  3. @Djisves New topic created : https://forums.getpaint.net/topic/116512-reconstruction-filter-tool-box/ Shall I destroy the current topic or let a moderator lock it ?
  4. Hello, The goal of the proposed plugin is to provide advanced settings for processing image using Sinc filters. It offers the following settings: - Kernel shape (square, disc and diamond) (diamond is not a shape really common) - Basic cutoff settings (Will be improved in future version) - Kernel radius, literally the size of the box filter in pixels - Border handling (uniform color, extend, mirror) - Global contrast The processing may be heavy in case of large radius; the plugin is practical but lack of optimization. I place the plugin under Effects -> Advanced. Here an illustration of the tool: - Inverse dithering - Anti-aliasing And the corresponding spectrum (originally for validation purpose but I found it is nice to show) The source code // Name: WaveToolBox // Submenu: Advanced // Author: Pascal Ollive // Title: Wave tool box // Version: 1.1 // Desc: Reconstruction filter tool box // Keywords: convolution|kernel|filter // URL: // Help: #region UICode RadioButtonControl kernelShape = 0; // Shape|Square|Disc|Diamond CheckboxControl sharpCutoff = false; // Sharp cutoff IntSliderControl kernelRadius = 8; // [8,100] Radius RadioButtonControl borderHandling = 2; // Border handling|Uniform|Extend|Mirror IntSliderControl contrastSliderIndex = 5; // [0,10] Contrast #endregion private const double SQRT_2 = 1.41421356237309505; private double[] kernelMatrix = null; private byte cutoffFactor = 1; private double kernelNorm = 1.0; private static byte to8bit(double x) { if (x < 0) { return 0; } if (x > 255) { return 255; } return (byte) Math.Round(x); } private static double lanczos(double x, int lobeCount) { if (x == 0.0) { return 1.0; } double xMultiplyPI = Math.PI * x; return lobeCount * Math.Sin(xMultiplyPI) * Math.Sin(xMultiplyPI / lobeCount) / (xMultiplyPI * xMultiplyPI); } private static double computeLanczosCoef(int i, int j, byte shape, int lobeCount, int radius) { // center shift double x = (i - radius + 1); double y = (j - radius + 1); if (shape == 0) { // Square // normalize into lanczos window return lanczos(lobeCount * x / radius, lobeCount) * lanczos(lobeCount * y / radius, lobeCount); } if (shape == 1) { // Disc double d = Math.Sqrt(x * x + y * y); if (d < radius) { // normalize into lanczos window return lanczos(lobeCount * d / radius, lobeCount); } } if (shape == 2) { // Diamond (45 degrees rotated square) double p = (x + y) * SQRT_2 / 2; if (p < radius) { // normalize into lanczos window return lanczos(lobeCount * p / radius, lobeCount) * lanczos(lobeCount * (p - SQRT_2 * y) / radius, lobeCount); } } return 0.0; } private static double[] createLanczosKernel(byte shape, int lobeCount, int radius) { int dimension = 2 * radius - 1; double[] matrix = new double[dimension * dimension]; for (int j = 0; j < dimension; j++) { for (int i = 0; i < dimension; i++) { matrix[dimension * j + i] = computeLanczosCoef(i, j, shape, lobeCount, radius); } } return matrix; } private static double getCoefficientSum(double[] matrix) { double result = matrix[0]; for (int i = 1; i < matrix.Length; i++) { result = result + matrix[i]; } return result; } private static int surfaceExtend(int x, int maxValue) { if (x < 0) { return 0; } if (x > maxValue) { return maxValue; } return x; } private static int surfaceMirror(int x, int maxValue) { if (x < 0) { return - x; } if (x > maxValue) { return 2 * maxValue - x; } return x; } private ColorBgra getSafeSurfacePixel(Surface s, int x, int y) { if ((x >= 0) && (y >= 0) && (x < s.Width) && (y < s.Height)) { return s[x, y]; } if (borderHandling > 0) { if (borderHandling == 1) { // Extend return s[surfaceExtend(x, s.Width - 1), surfaceExtend(y, s.Height - 1)]; } // Mirror return s[surfaceMirror(x, s.Width - 1), surfaceMirror(y, s.Height - 1)]; } // Background color return ColorBgra.Black; } private double[] convolve(Surface s, int x, int y, double[] m, int radius) { double[] bgr = new double[3]; int dimension = 2 * radius - 1; int offset = 0; for (int j = 0; j < dimension; j++) { for (int i = 0; i < dimension; i++) { ColorBgra c = getSafeSurfacePixel(s, x + i - radius + 1, y + j - radius + 1); bgr[0] = bgr[0] + c.B * m[offset]; bgr[1] = bgr[1] + c.G * m[offset]; bgr[2] = bgr[2] + c.R * m[offset]; offset = offset + 1; } } return bgr; } private double applyContrast(double x) { return (256 + 10 * (contrastSliderIndex - 5)) * x / 256 - (5 * (contrastSliderIndex - 5)); } void PreRender(Surface dst, Surface src) { cutoffFactor = (byte) (sharpCutoff ? 2 : 1); kernelMatrix = createLanczosKernel(kernelShape, 8 * cutoffFactor, kernelRadius * cutoffFactor); kernelNorm = getCoefficientSum(kernelMatrix); } void Render(Surface dst, Surface src, Rectangle rect) { for (int y = rect.Top; y < rect.Bottom; y++) { if (IsCancelRequested) return; for (int x = rect.Left; x < rect.Right; x++) { double[] bgr = convolve(src, x, y, kernelMatrix, kernelRadius * cutoffFactor); dst[x,y] = ColorBgra.FromBgr(to8bit(applyContrast(bgr[0] / kernelNorm)), to8bit(applyContrast(bgr[1] / kernelNorm)), to8bit(applyContrast(bgr[2] / kernelNorm))); } } } Best regards, WaveToolBox.zip
  5. Hello Djisves, Thanks for the feedback ! I called this plugin tool box because it can be used for many things and I think you're right render may not be a good place (I hesitated a lot in fact). I may change the location to advanced it would be more suitable. I test what I deliver so I can say it's ok to be run. I will fix the menu issue and add a parameter to tune the Sinc filter before delivering in Plugins - Publishing ONLY!. Have a nice day,
  6. Hi, I deliver the first version of the WaveToolBox plugin. The goal is to provide advanced settings for processing image using Sinc filters. This is work in progress but this first version already offers the following settings: - Kernel shape (square, disc and diamond) (diamond is not a shape really common) - Kernel radius, literally the size of the box filter in pixels - Border handling (uniform color, extend, mirror) - Global contrast The processing may be heavy in case of large radius; the plugin is practical but lack of optimization. I place the plugin under Effects -> Render. Here an illustration of the tool: - Inverse dithering - Anti-aliasing And the source code: // Name: WaveToolBox // Submenu: Render // Author: Pascal Ollive // Title: Wave tool box // Version: 1.0 // Desc: Reconstruction filter tool box // Keywords: convolution|kernel|filter // URL: // Help: #region UICode RadioButtonControl kernelShape = 0; // Shape|Square|Disc|Diamond IntSliderControl kernelRadius = 8; // [8,100] Radius RadioButtonControl borderHandling = 2; // Border handling|Uniform|Extend|Mirror IntSliderControl contrastSliderIndex = 5; // [0,10] Contrast #endregion private const double SQRT_2 = 1.41421356237309505; private double[] kernelMatrix = null; private double kernelNorm = 1.0; private static byte to8bit(double x) { if (x < 0) { return 0; } if (x > 255) { return 255; } return (byte) Math.Round(x); } private static double lanczos8(double x) { if (x == 0.0) { return 1.0; } double xMultiplyPI = Math.PI * x; return 8.0 * Math.Sin(xMultiplyPI) * Math.Sin(xMultiplyPI / 8.0) / (xMultiplyPI * xMultiplyPI); } private static double computeLanczosCoef(int i, int j, int radius, byte shape) { // center shift double x = (i - radius + 1); double y = (j - radius + 1); if (shape == 0) { // Square // normalize into lanczos8 window return lanczos8(8.0 * x / radius) * lanczos8(8.0 * y / radius); } if (shape == 1) { // Disc double d = Math.Sqrt(x * x + y * y); if (d < radius) { // normalize into lanczos8 window return lanczos8(8.0 * d / radius); } } if (shape == 2) { // Diamond (45 degrees rotated square) double p = (x + y) * SQRT_2 / 2; if (p < radius) { // normalize into lanczos8 window return lanczos8(8.0 * p / radius) * lanczos8(8.0 * (p - SQRT_2 * y) / radius); } } return 0.0; } private static double[] createLanczosKernel(int radius, byte shape) { int dimension = 2 * radius - 1; double[] matrix = new double[dimension * dimension]; for (int j = 0; j < dimension; j++) { for (int i = 0; i < dimension; i++) { matrix[dimension * j + i] = computeLanczosCoef(i, j, radius, shape); } } return matrix; } private static double getCoefficientSum(double[] matrix) { double result = matrix[0]; for (int i = 1; i < matrix.Length; i++) { result = result + matrix[i]; } return result; } private static int surfaceExtend(int x, int maxValue) { if (x < 0) { return 0; } if (x > maxValue) { return maxValue; } return x; } private static int surfaceMirror(int x, int maxValue) { if (x < 0) { return - x; } if (x > maxValue) { return 2 * maxValue - x; } return x; } private ColorBgra getSafeSurfacePixel(Surface s, int x, int y) { if ((x >= 0) && (y >= 0) && (x < s.Width) && (y < s.Height)) { return s[x, y]; } if (borderHandling > 0) { if (borderHandling == 1) { // Extend return s[surfaceExtend(x, s.Width - 1), surfaceExtend(y, s.Height - 1)]; } // Mirror return s[surfaceMirror(x, s.Width - 1), surfaceMirror(y, s.Height - 1)]; } // Background color return ColorBgra.Black; } private double[] convolve(Surface s, int x, int y, double[] m, int radius) { double[] bgr = new double[3]; int dimension = 2 * radius - 1; int offset = 0; for (int j = 0; j < dimension; j++) { for (int i = 0; i < dimension; i++) { ColorBgra c = getSafeSurfacePixel(s, x + i - radius + 1, y + j - radius + 1); bgr[0] = bgr[0] + c.B * m[offset]; bgr[1] = bgr[1] + c.G * m[offset]; bgr[2] = bgr[2] + c.R * m[offset]; offset = offset + 1; } } return bgr; } private double applyContrast(double x) { return (256 + 10 * (contrastSliderIndex - 5)) * x / 256 - (5 * (contrastSliderIndex - 5)); } void PreRender(Surface dst, Surface src) { kernelMatrix = createLanczosKernel(kernelRadius, kernelShape); kernelNorm = getCoefficientSum(kernelMatrix); } void Render(Surface dst, Surface src, Rectangle rect) { for (int y = rect.Top; y < rect.Bottom; y++) { if (IsCancelRequested) return; for (int x = rect.Left; x < rect.Right; x++) { double[] bgr = convolve(src, x, y, kernelMatrix, kernelRadius); dst[x,y] = ColorBgra.FromBgr(to8bit(applyContrast(bgr[0] / kernelNorm)), to8bit(applyContrast(bgr[1] / kernelNorm)), to8bit(applyContrast(bgr[2] / kernelNorm))); } } } WaveToolBox.zip
  7. Hi ! I modified the engine of my sharpening plugin, the sharpen amount is now following the standard. I changed how the sharpen is applied. The slider has a nonlinear effect on result allowing to have more range. (Math enthusiast will spot easily the sequence of values used). And the source code. // Name: Slight edge boost // Submenu: Photo // Author: Pascal Ollive // Title: Slight edge boost // Version: 1.4 // Desc: Gaussian unsharp mask // Keywords: Sharpening|Gaussian|filter // URL: // Help: #region UICode IntSliderControl sliderValue = 5; // [0,10] Strength #endregion // 10 amounts depending of sliderValue private static byte[] AMOUNT = {2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233}; // 2 * Math.exp(-6*x^2+y^2) + Math.exp(-9*x^2+y^2/8) r=1.5 // sum(matrix) = 0 // factor = 65536 private static int[] KERNEL7x7 = { -3, -34, -150, -248, -150, -34, -3, -34, -409, -1831, -3021, -1831, -409, -34, -150, -1831, -8423, -16633, -8423, -1831, -150, -248, -3021, -16633, 131068, -16633, -3021, -248, -150, -1831, -8423, -16633, -8423, -1831, -150, -34, -409, -1831, -3021, -1831, -409, -34, -3, -34, - 150, -248, -150, -34, -3 }; // Truncated matrix => border management private static int[] KERNEL5x5 = { -409, -1831, -3021, -1831, -409, -1831, -8423, -16633, -8423, -1831, -3021, -16633, 128592, -16633, -3021, -1831, -8423, -16633, -8423, -1831, -409, -1831, -3021, -1831, -409 }; private static int[] KERNEL3x3 = { -8423, -16633, -8423, -16633, 100224, -16633, -8423, -16633, -8423 }; private static int[][] KERNEL_ARRAY = { null, // identity KERNEL3x3, KERNEL5x5, KERNEL7x7 }; private long strength = 21; // [2,233] // Utility function to provide result in 8-bit range // strength 2^5 // factor 2^16 // 21-bit scale private static byte to8bit(long n) { long result = (n + 1048576) >> 21; if (result > 255) { return 255; } if (result < 0) { return 0; } return (byte) result; } // [ p0 ][center][ p2 ] <= Extend border // [ p0 ][center][ p2 ] (center = p1) // [ p3 ][ p4 ][ p5 ] private ColorBgra convolveBorder(ColorBgra[] arrayOfPoints) { long r = (KERNEL3x3[0] + KERNEL3x3[3]) * strength * arrayOfPoints[0].R + ((KERNEL3x3[1] + KERNEL3x3[4]) * strength + 2097152) * arrayOfPoints[1].R + (KERNEL3x3[2] + KERNEL3x3[5]) *strength * arrayOfPoints[2].R + KERNEL3x3[6] * strength * arrayOfPoints[3].R + KERNEL3x3[7] * strength * arrayOfPoints[4].R + KERNEL3x3[8] * strength * arrayOfPoints[5].R; long g = (KERNEL3x3[0] + KERNEL3x3[3]) * strength * arrayOfPoints[0].G + ((KERNEL3x3[1] + KERNEL3x3[4]) * strength + 2097152) * arrayOfPoints[1].G + (KERNEL3x3[2] + KERNEL3x3[5]) * strength * arrayOfPoints[2].G + KERNEL3x3[6] * strength * arrayOfPoints[3].G + KERNEL3x3[7] * strength * arrayOfPoints[4].G + KERNEL3x3[8] * strength * arrayOfPoints[5].G; long b = (KERNEL3x3[0] + KERNEL3x3[3]) * strength * arrayOfPoints[0].B + ((KERNEL3x3[1] + KERNEL3x3[4]) * strength + 2097152) * arrayOfPoints[1].B + (KERNEL3x3[2] + KERNEL3x3[5]) * strength * arrayOfPoints[2].B + KERNEL3x3[6] * strength * arrayOfPoints[3].B + KERNEL3x3[7] * strength * arrayOfPoints[4].B + KERNEL3x3[8] * strength * arrayOfPoints[5].B; return ColorBgra.FromBgr(to8bit(b), to8bit(g), to8bit(r)); } private void copyBorderLeft(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x, y - 1]; dest[1] = src[x, y]; dest[2] = src[x, y + 1]; dest[3] = src[x + 1, y - 1]; dest[4] = src[x + 1, y]; dest[5] = src[x + 1, y + 1]; } private void copyBorderRight(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x, y - 1]; dest[1] = src[x, y]; dest[2] = src[x, y + 1]; dest[3] = src[x - 1, y - 1]; dest[4] = src[x - 1, y]; dest[5] = src[x - 1, y + 1]; } private void copyBorderTop(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x - 1, y]; dest[1] = src[x, y]; dest[2] = src[x + 1, y]; dest[3] = src[x - 1, y + 1]; dest[4] = src[x, y + 1]; dest[5] = src[x + 1, y + 1]; } private void copyBorderBottom(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x - 1, y]; dest[1] = src[x, y]; dest[2] = src[x + 1, y]; dest[3] = src[x - 1, y - 1]; dest[4] = src[x, y - 1]; dest[5] = src[x + 1, y - 1]; } private ColorBgra convolve(Surface src, int x, int y, byte radius) { int[] kernel = KERNEL_ARRAY[radius]; long r = src[x, y].R << 21; long g = src[x, y].G << 21; long b = src[x, y].B << 21; int offset = 0; for (int j = y - radius; j <= y + radius; j++) { for (int i = x - radius; i <= x + radius; i++) { ColorBgra srcPixel = src[i, j]; r = r + kernel[offset] * strength * srcPixel.R; g = g + kernel[offset] * strength * srcPixel.G; b = b + kernel[offset] * strength * srcPixel.B; offset = offset + 1; } } return ColorBgra.FromBgr(to8bit(b), to8bit(g), to8bit(r)); } private ColorBgra processPixel(Surface src, int x, int y, ColorBgra[] arrayOfPoints) { if (((x == 0) || (x == src.Width - 1)) && ((y == 0) || (y == src.Height - 1))) { // Extreme corner => identity return src[x, y]; } if (x == 0) { copyBorderLeft(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (x == src.Width - 1) { copyBorderRight(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (y == 0) { copyBorderTop(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (y == src.Height - 1) { copyBorderBottom(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if ((x == 1) || (x == src.Width - 2) || (y == 1) || (y == src.Height - 2)) { return convolve(src, x, y, 1); } if ((x == 2) || (x == src.Width - 3) || (y == 2) || (y == src.Height - 3)) { return convolve(src, x, y, 2); } return convolve(src, x, y, 3); } void PreRender(Surface dst, Surface src) { // strength 5-bit precision [2/32 .. 233/32] strength = AMOUNT[sliderValue]; } void Render(Surface dst, Surface src, Rectangle rect) { ColorBgra[] borderPixels = new ColorBgra[6]; for (int y = rect.Top; y < rect.Bottom; y++) { if (IsCancelRequested) return; for (int x = rect.Left; x < rect.Right; x++) { dst[x, y] = processPixel(src, x, y, borderPixels); } } } SlightEdgeBoost.zip
  8. Testing is doubting I wrote the following loops to measure the result between the two methods for returning integer luminosity with the one returning the intensity in floating point. #if DEBUG int diffCount = 0; for (int b = 0; b < 256; b++) { for (int g = 0; g < 256; g++) { for (int r = 0; r < 256; r++) { ColorBgra c = ColorBgra.FromBgr((byte)b, (byte)g, (byte) r); byte loupasc = getIntensityFromBgr(c); byte intensity = c.GetIntensityByte(); if (loupasc != intensity) { if (Math.Abs(255 * c.GetIntensity() - loupasc) < Math.Abs(255 * c.GetIntensity() - intensity)) { diffCount = diffCount + 1; } } } } } Debug.WriteLine("diffCount=" + diffCount); #endif private static byte getIntensityFromBgr(ColorBgra c) { return (byte) ((7500 * c.B + 38619 * c.G + 19672 * c.R) >> 16); } Results 7 004 946 number of colors when results are closer to GetIntensity with the proposed coefficients than GetIntensityByte. Changing the sign of the inequality 1 310 945 number of colors when results are less accurate with the proposed coefficients than GetIntensityByte. The rest is equality.
  9. Hi, I noticed something about color to intensity conversion which is a bit disturbing for me but It might be insignificant. I use the debug mode: #if DEBUG Debug.WriteLine(ColorBgra.FromBgr(254, 255, 255).GetIntensityByte()); Debug.WriteLine(255 * ColorBgra.FromBgr(254, 255, 255).GetIntensity()); #endif The results 254 254,886 IntensityByte returns 254 but the expected value should be 255. I found this topic which explain the method to get intensity byte : https://forums.getpaint.net/topic/1119-colorbgracs/?tab=comments#comment-4952 I understand that Luma = 7471 * B + 38470 * G + 19595 * R >> 16 where 0.114 * 65536 = 7471 0.587 * 65536 = 38470 0.299 * 65536 = 19595 which seems right at the first sight (7471 + 38470 + 19595 = 65536). But something is missing causing an asymmetry between dark tones (0,1,2...) and bright tones (...,253,254,255). 255 - ColorBgra.FromBgr(1, 0, 0).GetIntensityByte() should be equals to ColorBgra.FromBgr(254, 255, 255).GetIntensityByte() It's because the implementation is considering the spreading of luma in values between 0 inclusive and 255 inclusive but It should be considered in values between 0 inclusive and 256 exclusive which produces different result because of the division by 65536 (right shift). Therefore intensities are not equally distributed across intensity range, 255 is wrongly represented. Consequently the sum of coefficients should not be equals to 65536 but equals to 65791 = 65536 + 256 - 1 (minus one for exclude the single value producing 256). I proposed these BGR coefficients : 0.114 * 65791= 7500 0.587 * 65791 = 38619 0.299 * 65791 = 19672 Applying these coefficients fix the distribution problem. Hope this help
  10. Hi ! This plugin uses a specific error diffusion method: Sub-block error diffusion. The block size is fixed to 3x3 in this plugin. The image is split into 3x3 boxes and boxes are replaced by a pattern selected from a fixed collection of patterns (small squares in this plugin). Patterns contain black and white pixels. Errors caused by the replacement is diffused to the neighbor pixels. The algorithm is described here http://caca.zoy.org/wiki/libcaca/study/3 This algorithm is well suited to generate ASCII art image with the right collection patterns and box size. Result: And the source code // Name: Blockifier // Submenu: Stylize // Author: Pascal Ollive // Title: Blockifier // Version: 1.0 // Desc: Binary sub-block error diffusion // Keywords: sub-block|diffusion|dither|binary // URL: // Help: #region UICode #endregion private static byte[] pattern0 = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; private static byte[] pattern1 = { 255, 255, 255, 255, 255, 255, 255, 255, 255 }; private static byte[] pattern2 = { 255, 255, 0, 255, 255, 0, 0, 0, 0 }; private static byte[] pattern3 = { 0, 255, 255, 0, 255, 255, 0, 0, 0 }; private static byte[] pattern4 = { 0, 0, 0, 0, 255, 255, 0, 255, 255 }; private static byte[] pattern5 = { 0, 0, 0, 255, 255, 0, 255, 255, 0 }; private static byte[] pattern6 = { 0, 0, 0, 0, 255, 0, 0, 0, 0 }; private static byte[] pattern7 = { 255, 0, 0, 0, 0, 0, 0, 0, 0 }; private static byte[] pattern8 = { 255, 0, 0, 255, 0, 0, 0, 0, 0 }; private static byte[][] arrayOfPatterns = { pattern0, pattern1, pattern2, pattern3, pattern4, pattern5, pattern6, pattern7, pattern8 }; private static byte getSafeSurfacePixel(Surface s, int x, int y) { if ((x >= 0) && (y >= 0) && (x < s.Width) && (y < s.Height)) { return s[x, y].GetIntensityByte(); } return 0; } private static void setSafeSurfacePixel(Surface s, int x, int y, byte luma8bit) { if ((x >= 0) && (y >= 0) && (x < s.Width) && (y < s.Height)) { s[x, y] = ColorBgra.FromBgr(luma8bit, luma8bit, luma8bit); } } private static void copyPattern(byte[] pattern, Surface s, int x, int y) { setSafeSurfacePixel(s, x, y, pattern[0]); setSafeSurfacePixel(s, x + 1, y, pattern[1]); setSafeSurfacePixel(s, x + 2, y, pattern[2]); setSafeSurfacePixel(s, x, y + 1, pattern[3]); setSafeSurfacePixel(s, x + 1, y + 1, pattern[4]); setSafeSurfacePixel(s, x + 2, y + 1, pattern[5]); setSafeSurfacePixel(s, x, y + 2, pattern[6]); setSafeSurfacePixel(s, x + 1, y + 2, pattern[7]); setSafeSurfacePixel(s, x + 2, y + 2, pattern[8]); } private static byte div64(int n) { if (n > 16351) { return 255; } if (n < 0) { return 0; } return (byte) ((n + 32) / 64); } private static void diffuseError(byte[] pattern, Surface s, int x, int y) { // [a][b][c] 0 // [d][e][f] 1 // [g][h][i] 2 //3 4 5 6 7 int[] pixelTemp = new int[8]; pixelTemp[0] = 64 * getSafeSurfacePixel(s, x + 3, y); pixelTemp[1] = 64 * getSafeSurfacePixel(s, x + 3, y + 1); pixelTemp[2] = 64 * getSafeSurfacePixel(s, x + 3, y + 2); pixelTemp[3] = 64 * getSafeSurfacePixel(s, x - 1, y + 3); pixelTemp[4] = 64 * getSafeSurfacePixel(s, x, y + 3); pixelTemp[5] = 64 * getSafeSurfacePixel(s, x + 1, y + 3); pixelTemp[6] = 64 * getSafeSurfacePixel(s, x + 2, y + 3); pixelTemp[7] = 64 * getSafeSurfacePixel(s, x + 3, y + 3); // [a] int error = getSafeSurfacePixel(s, x, y) - pattern[0]; pixelTemp[0] = pixelTemp[0] + 2 * error; pixelTemp[1] = pixelTemp[1] + 5 * error; pixelTemp[2] = pixelTemp[2] + 6 * error; pixelTemp[3] = pixelTemp[3] + 5 * error; pixelTemp[4] = pixelTemp[4] + 17 * error; pixelTemp[5] = pixelTemp[5] + 17 * error; pixelTemp[6] = pixelTemp[6] + 9 * error; pixelTemp[7] = pixelTemp[7] + error; // [b] error = getSafeSurfacePixel(s, x + 1, y) - pattern[1]; pixelTemp[0] = pixelTemp[0] + 6 * error; pixelTemp[1] = pixelTemp[1] + 9 * error; pixelTemp[2] = pixelTemp[2] + 8 * error; pixelTemp[3] = pixelTemp[3] + 2 * error; pixelTemp[4] = pixelTemp[4] + 11 * error; pixelTemp[5] = pixelTemp[5] + 16 * error; pixelTemp[6] = pixelTemp[6] + 11 * error; pixelTemp[7] = pixelTemp[7] + error; // [c] error = getSafeSurfacePixel(s, x + 2, y) - pattern[2]; pixelTemp[0] = pixelTemp[0] + 20 * error; pixelTemp[1] = pixelTemp[1] + 14 * error; pixelTemp[2] = pixelTemp[2] + 8 * error; pixelTemp[4] = pixelTemp[4] + 3 * error; pixelTemp[5] = pixelTemp[5] + 9 * error; pixelTemp[6] = pixelTemp[6] + 9 * error; pixelTemp[7] = pixelTemp[7] + error; // [d] error = getSafeSurfacePixel(s, x, y + 1) - pattern[3]; pixelTemp[1] = pixelTemp[1] + 2 * error; pixelTemp[2] = pixelTemp[2] + 5 * error; pixelTemp[3] = pixelTemp[3] + 7 * error; pixelTemp[4] = pixelTemp[4] + 23 * error; pixelTemp[5] = pixelTemp[5] + 18 * error; pixelTemp[6] = pixelTemp[6] + 8 * error; pixelTemp[7] = pixelTemp[7] + error; // [e] error = getSafeSurfacePixel(s, x + 1, y + 1) - pattern[4]; pixelTemp[1] = pixelTemp[1] + 6 * error; pixelTemp[2] = pixelTemp[2] + 9 * error; pixelTemp[3] = pixelTemp[3] + 2 * error; pixelTemp[4] = pixelTemp[4] + 12 * error; pixelTemp[5] = pixelTemp[5] + 21 * error; pixelTemp[6] = pixelTemp[6] + 13 * error; pixelTemp[7] = pixelTemp[7] + error; // [f] error = getSafeSurfacePixel(s, x + 2, y + 1) - pattern[5]; pixelTemp[1] = pixelTemp[1] + 20 * error; pixelTemp[2] = pixelTemp[2] + 14 * error; pixelTemp[4] = pixelTemp[4] + 2 * error; pixelTemp[5] = pixelTemp[5] + 11 * error; pixelTemp[6] = pixelTemp[6] + 15 * error; pixelTemp[7] = pixelTemp[7] + 2 * error; // [g] error = getSafeSurfacePixel(s, x, y + 2) - pattern[6]; pixelTemp[2] = pixelTemp[2] + 2 * error; pixelTemp[3] = pixelTemp[3] + 12 * error; pixelTemp[4] = pixelTemp[4] + 32 * error; pixelTemp[5] = pixelTemp[5] + 14 * error; pixelTemp[6] = pixelTemp[6] + 4 * error; // [h] error = getSafeSurfacePixel(s, x + 1, y + 2) - pattern[7]; pixelTemp[2] = pixelTemp[2] + 6 * error; pixelTemp[4] = pixelTemp[4] + 12 * error; pixelTemp[5] = pixelTemp[5] + 32 * error; pixelTemp[6] = pixelTemp[6] + 13 * error; pixelTemp[7] = pixelTemp[7] + error; // [i] error = getSafeSurfacePixel(s, x + 2, y + 2) - pattern[8]; pixelTemp[2] = pixelTemp[2] + 20 * error; pixelTemp[5] = pixelTemp[5] + 12 * error; pixelTemp[6] = pixelTemp[6] + 28 * error; pixelTemp[7] = pixelTemp[7] + 4 * error; // Apply error diffusion setSafeSurfacePixel(s, x + 3, y, div64(pixelTemp[0])); setSafeSurfacePixel(s, x + 3, y + 1, div64(pixelTemp[1])); setSafeSurfacePixel(s, x + 3, y + 2, div64(pixelTemp[2])); setSafeSurfacePixel(s, x - 1, y + 3, div64(pixelTemp[3])); setSafeSurfacePixel(s, x, y + 3, div64(pixelTemp[4])); setSafeSurfacePixel(s, x + 1, y + 3, div64(pixelTemp[5])); setSafeSurfacePixel(s, x + 2, y + 3, div64(pixelTemp[6])); setSafeSurfacePixel(s, x + 3, y + 3, div64(pixelTemp[7])); } // Weighted comparison (upper left pixel harder to compensate) // [ 8 7 5 ] // [ 7 6 5 ] // [ 5 5 4 ] // 3x3 pixel square private static int getPatternDiff(Surface s, int x, int y, byte[] pattern) { int patternDiff = Math.Abs(getSafeSurfacePixel(s, x, y) - pattern[0] + getSafeSurfacePixel(s, x + 1, y ) - pattern[1] + getSafeSurfacePixel(s, x + 2, y ) - pattern[2] + getSafeSurfacePixel(s, x, y + 1) - pattern[3] + getSafeSurfacePixel(s, x + 1, y + 1) - pattern[4] + getSafeSurfacePixel(s, x + 2, y + 1) - pattern[5] + getSafeSurfacePixel(s, x, y + 2) - pattern[6] + getSafeSurfacePixel(s, x + 1, y + 2) - pattern[7] + getSafeSurfacePixel(s, x + 2, y + 2) - pattern[8]); int weightedError = (8 * Math.Abs(getSafeSurfacePixel(s, x, y) - pattern[0]) + 7 * Math.Abs(getSafeSurfacePixel(s, x + 1, y ) - pattern[1]) + 5 * Math.Abs(getSafeSurfacePixel(s, x + 2, y ) - pattern[2]) + 7 * Math.Abs(getSafeSurfacePixel(s, x, y + 1) - pattern[3]) + 6 * Math.Abs(getSafeSurfacePixel(s, x + 1, y + 1) - pattern[4]) + 5 * Math.Abs(getSafeSurfacePixel(s, x + 2, y + 1) - pattern[5]) + 5 * Math.Abs(getSafeSurfacePixel(s, x, y + 2) - pattern[6]) + 5 * Math.Abs(getSafeSurfacePixel(s, x + 1, y + 2) - pattern[7]) + 4 * Math.Abs(getSafeSurfacePixel(s, x + 2, y + 2) - pattern[8])); return 52 * patternDiff + 9 * weightedError; } void Render(Surface dst, Surface src, Rectangle rect) { byte[] closestPattern = null; int arrayOfPatternLength = 0; int left = rect.Left / 3; int top = rect.Top / 3; int nbHorizontalBlock = (rect.Right - (3 * left) + 2) / 3; int nbVerticalBlock = (rect.Bottom - (3 * top) + 2) / 3; dst.CopySurface(src, rect.Location, rect); for (int y = 0; y < nbVerticalBlock; y++) { if (IsCancelRequested) return; for (int x = 0; x < nbHorizontalBlock; x++) { if (closestPattern == pattern3) { // include extra pattern arrayOfPatternLength = arrayOfPatterns.Length; } else { arrayOfPatternLength = arrayOfPatterns.Length - 1; } closestPattern = arrayOfPatterns[0]; int diffMin = getPatternDiff(dst, 3 * (x + left), 3 * (y + top), arrayOfPatterns[0]); for (int p = 1; p < arrayOfPatternLength; p++) { int diff = getPatternDiff(dst, 3 * (x + left), 3 * (y + top), arrayOfPatterns[p]); if (diff < diffMin) { closestPattern = arrayOfPatterns[p]; diffMin = diff; } } diffuseError(closestPattern, dst, 3 * (x + left), 3 * (y + top)); copyPattern(closestPattern, dst, 3 * (x + left), 3 * (y + top)); } } } Blockifier.zip
  11. I just realized that I can get surface dimension with Width and Height properties. Sorry for the last release... The good new is that the artifact on non-rectangular selection is fixed ! Changes are minor but I think it's good to post. // Name: Slight edge boost // Submenu: Photo // Author: Pascal Ollive // Title: Slight edge boost // Version: 1.3 // Desc: Gaussian unsharp mask // Keywords: Sharpening|Gaussian|filter // URL: // Help: #region UICode IntSliderControl sliderValue = 5; // [0,10] Strength #endregion // 2 * Math.exp(-6*x^2+y^2) + Math.exp(-9*x^2+y^2/8) r=1.5 // sum(matrix) = 16384 // factor = 16384 private static int[] KERNEL7x7 = { -1, -8, -38, -62, -38, -8, -1, -8, -102, -458, -755, -458, -102, -8, -38, -458, -2105, -4158, -2105, -458, -38, -62, -755, -4158, 49148, -4158, -755, -62, -38, -458, -2105, -4158, -2105, -458, -38, -8, -102, -458, -755, -458, -102, -8, -1, -8, -38, -62, -38, -8, -1 }; // Truncated matrix => border management private static int[] KERNEL5x5 = { -102, -458, -755, -458, -102, -458, -2105, -4158, -2105, -458, -755, -4158, 48528, -4158, -755, -458, -2105, -4158, -2105, -458, -102, -458, -755, -458, -102 }; private static int[] KERNEL3x3 = { -2105, -4158, -2105, -4158, 41436, -4158, -2105, -4158, -2105 }; private static int[][] KERNEL_ARRAY = { null, // identity KERNEL3x3, KERNEL5x5, KERNEL7x7 }; private byte strength = 17; // [2,32] // Function applied separately on each BGR component // The result of the sharpness depends of the user input private byte getSharpenedSubPixel(byte srcPixel, int sharpestValue) { int result = ((32 - strength) * (srcPixel << 14) + strength * sharpestValue + 262144) >> 19; if (result < 0) { return 0; } if (result > 255) { return 255; } return (byte) result; } // [ p0 ][center][ p2 ] <= Extend border // [ p0 ][center][ p2 ] (center = p1) // [ p3 ][ p4 ][ p5 ] private ColorBgra convolveBorder(ColorBgra[] arrayOfPoints) { int r = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].R + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].R + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].R + KERNEL3x3[6] * arrayOfPoints[3].R + KERNEL3x3[7] * arrayOfPoints[4].R + KERNEL3x3[8] * arrayOfPoints[5].R; int g = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].G + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].G + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].G + KERNEL3x3[6] * arrayOfPoints[3].G + KERNEL3x3[7] * arrayOfPoints[4].G + KERNEL3x3[8] * arrayOfPoints[5].G; int b = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].B + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].B + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].B + KERNEL3x3[6] * arrayOfPoints[3].B + KERNEL3x3[7] * arrayOfPoints[4].B + KERNEL3x3[8] * arrayOfPoints[5].B; return ColorBgra.FromBgr(getSharpenedSubPixel(arrayOfPoints[1].B, b), getSharpenedSubPixel(arrayOfPoints[1].G, g), getSharpenedSubPixel(arrayOfPoints[1].R, r)); } private void copyBorderLeft(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x, y - 1]; dest[1] = src[x, y]; dest[2] = src[x, y + 1]; dest[3] = src[x + 1, y - 1]; dest[4] = src[x + 1, y]; dest[5] = src[x + 1, y + 1]; } private void copyBorderRight(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x, y - 1]; dest[1] = src[x, y]; dest[2] = src[x, y + 1]; dest[3] = src[x - 1, y - 1]; dest[4] = src[x - 1, y]; dest[5] = src[x - 1, y + 1]; } private void copyBorderTop(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x - 1, y]; dest[1] = src[x, y]; dest[2] = src[x + 1, y]; dest[3] = src[x - 1, y + 1]; dest[4] = src[x, y + 1]; dest[5] = src[x + 1, y + 1]; } private void copyBorderBottom(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x - 1, y]; dest[1] = src[x, y]; dest[2] = src[x + 1, y]; dest[3] = src[x - 1, y - 1]; dest[4] = src[x, y - 1]; dest[5] = src[x + 1, y - 1]; } private ColorBgra convolve(Surface src, int x, int y, byte radius) { int[] kernel = KERNEL_ARRAY[radius]; int r = 0; int g = 0; int b = 0; int offset = 0; for (int j = y - radius; j <= y + radius; j++) { for (int i = x - radius; i <= x + radius; i++) { ColorBgra srcPixel = src[i, j]; r = r + kernel[offset] * srcPixel.R; g = g + kernel[offset] * srcPixel.G; b = b + kernel[offset] * srcPixel.B; offset = offset + 1; } } return ColorBgra.FromBgr(getSharpenedSubPixel(src[x, y].B, b), getSharpenedSubPixel(src[x, y].G, g), getSharpenedSubPixel(src[x, y].R, r)); } private ColorBgra processPixel(Surface src, int x, int y, ColorBgra[] arrayOfPoints) { if (((x == 0) || (x == src.Width - 1)) && ((y == 0) || (y == src.Height - 1))) { // Extreme corner => identity return src[x, y]; } if (x == 0) { copyBorderLeft(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (x == src.Width - 1) { copyBorderRight(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (y == 0) { copyBorderTop(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (y == src.Height - 1) { copyBorderBottom(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if ((x == 1) || (x == src.Width - 2) || (y == 1) || (y == src.Height - 2)) { return convolve(src, x, y, 1); } if ((x == 2) || (x == src.Width - 3) || (y == 2) || (y == src.Height - 3)) { return convolve(src, x, y, 2); } return convolve(src, x, y, 3); } void PreRender(Surface dst, Surface src) { // 10 => 32 for fast division strength = (byte) (3 * sliderValue + 2); } void Render(Surface dst, Surface src, Rectangle rect) { ColorBgra[] borderPixels = new ColorBgra[6]; for (int y = rect.Top; y < rect.Bottom; y++) { if (IsCancelRequested) return; for (int x = rect.Left; x < rect.Right; x++) { dst[x, y] = processPixel(src, x, y, borderPixels); } } }
  12. The unsharp amount has been increased causing coefficients being outside short integer range => values are now stored into 32-bit integer. The amount has been tuned; the goal is get the plugin sample photo sharp using the plugin once. The extra amount has been removed (this was a bad idea ^^) in order to keep the code simple. Smaller kernel matrix have been computed from the 7x7 kernel, 5x5 matrix loose the extreme border coefficients which are no longer compensated by the center coefficient => border coefficients keep the same value and the center value decreases, and idem for 3x3 matrix. TODO : improve border management in order avoid filter artifact on non-rectangular selection. // Name: Slight edge boost // Submenu: Photo // Author: Pascal Ollive // Title: Slight edge boost // Version: 1.2 // Desc: Gaussian unsharp mask // Keywords: Sharpening|Gaussian|filter // URL: // Help: #region UICode IntSliderControl sliderValue = 5; // [0,10] Strength #endregion // 2 * Math.exp(-6*x^2+y^2) + Math.exp(-9*x^2+y^2/8) r=1.5 // sum(matrix) = 16384 // factor = 16384 private static int[] KERNEL7x7 = { -1, -8, -38, -62, -38, -8, -1, -8, -102, -458, -755, -458, -102, -8, -38, -458, -2105, -4158, -2105, -458, -38, -62, -755, -4158, 49148, -4158, -755, -62, -38, -458, -2105, -4158, -2105, -458, -38, -8, -102, -458, -755, -458, -102, -8, -1, -8, -38, -62, -38, -8, -1 }; // Truncated matrix => border management private static int[] KERNEL5x5 = { -102, -458, -755, -458, -102, -458, -2105, -4158, -2105, -458, -755, -4158, 48528, -4158, -755, -458, -2105, -4158, -2105, -458, -102, -458, -755, -458, -102 }; private static int[] KERNEL3x3 = { -2105, -4158, -2105, -4158, 41436, -4158, -2105, -4158, -2105 }; private static int[][] KERNEL_ARRAY = { null, // identity KERNEL3x3, KERNEL5x5, KERNEL7x7 }; private byte strength = 17; // [2,32] // Function applied separately on each BGR component // The result of the sharpness depends of the user input private byte getSharpenedSubPixel(byte srcPixel, int sharpestValue) { int result = ((32 - strength) * (srcPixel << 14) + strength * sharpestValue + 262144) >> 19; if (result < 0) { return 0; } if (result > 255) { return 255; } return (byte) result; } // [ p0 ][center][ p2 ] <= Extend border // [ p0 ][center][ p2 ] (center = p1) // [ p3 ][ p4 ][ p5 ] private ColorBgra convolveBorder(ColorBgra[] arrayOfPoints) { int r = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].R + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].R + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].R + KERNEL3x3[6] * arrayOfPoints[3].R + KERNEL3x3[7] * arrayOfPoints[4].R + KERNEL3x3[8] * arrayOfPoints[5].R; int g = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].G + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].G + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].G + KERNEL3x3[6] * arrayOfPoints[3].G + KERNEL3x3[7] * arrayOfPoints[4].G + KERNEL3x3[8] * arrayOfPoints[5].G; int b = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].B + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].B + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].B + KERNEL3x3[6] * arrayOfPoints[3].B + KERNEL3x3[7] * arrayOfPoints[4].B + KERNEL3x3[8] * arrayOfPoints[5].B; return ColorBgra.FromBgr(getSharpenedSubPixel(arrayOfPoints[1].B, b), getSharpenedSubPixel(arrayOfPoints[1].G, g), getSharpenedSubPixel(arrayOfPoints[1].R, r)); } private void copyBorderLeft(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x, y - 1]; dest[1] = src[x, y]; dest[2] = src[x, y + 1]; dest[3] = src[x + 1, y - 1]; dest[4] = src[x + 1, y]; dest[5] = src[x + 1, y + 1]; } private void copyBorderRight(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x, y - 1]; dest[1] = src[x, y]; dest[2] = src[x, y + 1]; dest[3] = src[x - 1, y - 1]; dest[4] = src[x - 1, y]; dest[5] = src[x - 1, y + 1]; } private void copyBorderTop(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x - 1, y]; dest[1] = src[x, y]; dest[2] = src[x + 1, y]; dest[3] = src[x - 1, y + 1]; dest[4] = src[x, y + 1]; dest[5] = src[x + 1, y + 1]; } private void copyBorderBottom(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x - 1, y]; dest[1] = src[x, y]; dest[2] = src[x + 1, y]; dest[3] = src[x - 1, y - 1]; dest[4] = src[x, y - 1]; dest[5] = src[x + 1, y - 1]; } private ColorBgra convolve(Surface src, int x, int y, byte radius) { int[] kernel = KERNEL_ARRAY[radius]; int r = 0; int g = 0; int b = 0; int offset = 0; for (int j = y - radius; j <= y + radius; j++) { for (int i = x - radius; i <= x + radius; i++) { ColorBgra srcPixel = src[i, j]; r = r + kernel[offset] * srcPixel.R; g = g + kernel[offset] * srcPixel.G; b = b + kernel[offset] * srcPixel.B; offset = offset + 1; } } return ColorBgra.FromBgr(getSharpenedSubPixel(src[x, y].B, b), getSharpenedSubPixel(src[x, y].G, g), getSharpenedSubPixel(src[x, y].R, r)); } private ColorBgra processPixel(Surface src, int x, int y, Rectangle bounds, ColorBgra[] arrayOfPoints) { if (((x == bounds.Left) || (x == bounds.Right - 1)) && ((y == bounds.Top) || (y == bounds.Bottom - 1))) { // Extreme corner => identity return src[x, y]; } if (x == bounds.Left) { copyBorderLeft(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (x == bounds.Right - 1) { copyBorderRight(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (y == bounds.Top) { copyBorderTop(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (y == bounds.Bottom - 1) { copyBorderBottom(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if ((x == bounds.Left + 1) || (x == bounds.Right - 2) || (y == bounds.Top + 1) || (y == bounds.Bottom - 2)) { return convolve(src, x, y, 1); } if ((x == bounds.Left + 2) || (x == bounds.Right - 3) || (y == bounds.Top + 2) || (y == bounds.Bottom - 3)) { return convolve(src, x, y, 2); } return convolve(src, x, y, 3); } void PreRender(Surface dst, Surface src) { // 10 => 32 for fast division strength = (byte) (3 * sliderValue + 2); } void Render(Surface dst, Surface src, Rectangle rect) { ColorBgra[] borderPixels = new ColorBgra[6]; for (int y = rect.Top; y < rect.Bottom; y++) { if (IsCancelRequested) return; for (int x = rect.Left; x < rect.Right; x++) { dst[x, y] = processPixel(src, x, y, rect, borderPixels); } } }
  13. Hello, I updated this plugin and I added an extra sharpen strength. // Name: Slight edge boost // Submenu: Photo // Author: Pascal Ollive // Title: Slight edge boost // Version: 1.1 // Desc: Gaussian unsharp mask // Keywords: Sharpening|Gaussian|filter // URL: // Help: #region UICode IntSliderControl sliderValue = 5; // [0,11] Strength #endregion // sliderValue in [0,10] // 2 * Math.exp(-6*x^2+y^2) + Math.exp(-9*x^2+y^2/8) r=1.5 // sum(matrix) = 16384 (2^14) private static short[] SHARPER_KERNEL = { 0, -3, -14, -23, -14, -3, 0, -3, -38, -172, -284, -172, -38, -3, -14, -172, -793, -1566, -793, -172, -14, -23, -284, -1566, 28712, -1566, -284, -23, -14, -172, -793, -1566, -793, -172, -14, -3, -38, -172, -284, -172, -38, -3, 0, -3, -14, -23, -14, -3, 0 }; // sliderValue = 11 // 2 * Math.exp(-6*x^2+y^2) + Math.exp(-9*x^2+y^2/8) r=2 // sum(matrix) = 16384 private static short[] SHARPEST_KERNEL = { -8, -35, -80, -107, -80, -35, -8, -35, -141, -330, -442, -330, -141, -35, -80, -330, -897, -1610, -897, -330, -80, -107, -442, -1610, 32764, -1610, -442, -107, -80, -330, -897, -1610, -897, -330, -80, -35, -141, -330, -442, -330, -141, -35, -8, -35, -80, -107, -80, -35, -8 }; // Truncated matrix => border management private static short[] KERNEL5x5 = { -38, -170, -281, -170, -38, -170, -782, -1545, -782, -170, -281, -1545, 28328, -1545, -281, -170, -782, -1545, -782, -170, -38, -170, -281, -170, -38 }; private static short[] KERNEL3x3 = { -1037, -2048, -1037, -2048, 28724, -2048, -1037, -2048, -1037 }; private short[][] sharpenKernel = { null, // identity KERNEL3x3, KERNEL5x5, null // SHARPER_KERNEL or SHARPEST_KERNEL }; private byte strength = 17; // [2,32] // Function applied separately on each BGR component // The result of the sharpness depends of the user input private byte getSharpenedSubPixel(byte srcPixel, int sharpestValue) { int result = ((32 - strength) * (srcPixel << 14) + strength * sharpestValue + 262144) >> 19; if (result < 0) { return 0; } if (result > 255) { return 255; } return (byte) result; } // [ p0 ][center][ p2 ] <= Extend border // [ p0 ][center][ p2 ] (center = p1) // [ p3 ][ p4 ][ p5 ] private ColorBgra convolveBorder(ColorBgra[] arrayOfPoints) { int r = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].R + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].R + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].R + KERNEL3x3[6] * arrayOfPoints[3].R + KERNEL3x3[7] * arrayOfPoints[4].R + KERNEL3x3[8] * arrayOfPoints[5].R; int g = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].G + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].G + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].G + KERNEL3x3[6] * arrayOfPoints[3].G + KERNEL3x3[7] * arrayOfPoints[4].G + KERNEL3x3[8] * arrayOfPoints[5].G; int b = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].B + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].B + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].B + KERNEL3x3[6] * arrayOfPoints[3].B + KERNEL3x3[7] * arrayOfPoints[4].B + KERNEL3x3[8] * arrayOfPoints[5].B; return ColorBgra.FromBgr(getSharpenedSubPixel(arrayOfPoints[1].B, b), getSharpenedSubPixel(arrayOfPoints[1].G, g), getSharpenedSubPixel(arrayOfPoints[1].R, r)); } private void copyBorderLeft(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x, y - 1]; dest[1] = src[x, y]; dest[2] = src[x, y + 1]; dest[3] = src[x + 1, y - 1]; dest[4] = src[x + 1, y]; dest[5] = src[x + 1, y + 1]; } private void copyBorderRight(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x, y - 1]; dest[1] = src[x, y]; dest[2] = src[x, y + 1]; dest[3] = src[x - 1, y - 1]; dest[4] = src[x - 1, y]; dest[5] = src[x - 1, y + 1]; } private void copyBorderTop(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x - 1, y]; dest[1] = src[x, y]; dest[2] = src[x + 1, y]; dest[3] = src[x - 1, y + 1]; dest[4] = src[x, y + 1]; dest[5] = src[x + 1, y + 1]; } private void copyBorderBottom(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x - 1, y]; dest[1] = src[x, y]; dest[2] = src[x + 1, y]; dest[3] = src[x - 1, y - 1]; dest[4] = src[x, y - 1]; dest[5] = src[x + 1, y - 1]; } private ColorBgra convolve(Surface src, int x, int y, byte radius) { short[] kernel = sharpenKernel[radius]; int r = 0; int g = 0; int b = 0; int offset = 0; for (int j = y - radius; j <= y + radius; j++) { for (int i = x - radius; i <= x + radius; i++) { ColorBgra srcPixel = src[i, j]; r = r + kernel[offset] * srcPixel.R; g = g + kernel[offset] * srcPixel.G; b = b + kernel[offset] * srcPixel.B; offset = offset + 1; } } return ColorBgra.FromBgr(getSharpenedSubPixel(src[x, y].B, b), getSharpenedSubPixel(src[x, y].G, g), getSharpenedSubPixel(src[x, y].R, r)); } private ColorBgra processPixel(Surface src, int x, int y, Rectangle bounds, ColorBgra[] arrayOfPoints) { if (((x == bounds.Left) || (x == bounds.Right - 1)) && ((y == bounds.Top) || (y == bounds.Bottom - 1))) { // Extreme corner => identity return src[x, y]; } if (x == bounds.Left) { copyBorderLeft(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (x == bounds.Right - 1) { copyBorderRight(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (y == bounds.Top) { copyBorderTop(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (y == bounds.Bottom - 1) { copyBorderBottom(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if ((x == bounds.Left + 1) || (x == bounds.Right - 2) || (y == bounds.Top + 1) || (y == bounds.Bottom - 2)) { return convolve(src, x, y, 1); } if ((x == bounds.Left + 2) || (x == bounds.Right - 3) || (y == bounds.Top + 2) || (y == bounds.Bottom - 3)) { return convolve(src, x, y, 2); } return convolve(src, x, y, 3); } void PreRender(Surface dst, Surface src) { if (sliderValue > 10) { strength = 32; sharpenKernel[3] = SHARPEST_KERNEL; } else { // 10 => 32 for fast division strength = (byte) (3 * sliderValue + 2); sharpenKernel[3] = SHARPER_KERNEL; } } void Render(Surface dst, Surface src, Rectangle rect) { ColorBgra[] borderPixels = new ColorBgra[6]; for (int y = rect.Top; y < rect.Bottom; y++) { if (IsCancelRequested) return; for (int x = rect.Left; x < rect.Right; x++) { dst[x, y] = processPixel(src, x, y, rect, borderPixels); } } }
  14. Hi ! I wrote an image sharpen plugin. The processing is based on unsharp mask; the particularity of this filter is to use a different function : f(x,y) = 2 * e^(-6(x^2+y^2)) + e^(-9(x^2+y^2)/8) (r=1.5) The processing result seems to look good but I'm not the best judge The image attached come from a pinhole photography and the processing has been applied three times with the maximum amount. // Name: Slight edge boost // Submenu: Photo // Author: Pascal Ollive // Title: Slight edge boost // Version: 1.0 // Desc: Gaussian unsharp mask // Keywords: Sharpening|Gaussian|filter // URL: // Help: #region UICode IntSliderControl sliderValue = 5; // [0,10] Strength #endregion // 2 * Math.exp(-6*x^2+y^2) + Math.exp(-9*x^2+y^2/8) // sum(matrix) = 16384 (2^14) private static short[] KERNEL7x7 = { 0, -3, -14, -23, -14, -3, 0, -3, -38, -172, -284, -172, -38, -3, -14, -172, -793, -1566, -793, -172, -14, -23, -284, -1566, 28712, -1566, -284, -23, -14, -172, -793, -1566, -793, -172, -14, -3, -38, -172, -284, -172, -38, -3, 0, -3, -14, -23, -14, -3, 0 }; // Truncated matrix => border management private static short[] KERNEL5x5 = { -38, -170, -281, -170, -38, -170, -782, -1545, -782, -170, -281, -1545, 28328, -1545, -281, -170, -782, -1545, -782, -170, -38, -170, -281, -170, -38 }; private static short[] KERNEL3x3 = { -1037, -2048, -1037, -2048, 28724, -2048, -1037, -2048, -1037 }; private static short[][] KERNEL_ARRAY = { null, // identity KERNEL3x3, KERNEL5x5, KERNEL7x7 }; private byte strength = 17; // [2,32] // Function applied separately on each BGR component // The result of the sharpness depends of the user input private byte getSharpenedSubPixel(byte srcPixel, int sharpestValue) { int result = ((32 - strength) * (srcPixel << 14) + strength * sharpestValue + 262144) >> 19; if (result < 0) { return 0; } if (result > 255) { return 255; } return (byte) result; } // [ p0 ][center][ p2 ] <= Extend border // [ p0 ][center][ p2 ] (center = p1) // [ p3 ][ p4 ][ p5 ] private ColorBgra convolveBorder(ColorBgra[] arrayOfPoints) { int r = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].R + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].R + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].R + KERNEL3x3[6] * arrayOfPoints[3].R + KERNEL3x3[7] * arrayOfPoints[4].R + KERNEL3x3[8] * arrayOfPoints[5].R; int g = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].G + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].G + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].G + KERNEL3x3[6] * arrayOfPoints[3].G + KERNEL3x3[7] * arrayOfPoints[4].G + KERNEL3x3[8] * arrayOfPoints[5].G; int b = (KERNEL3x3[0] + KERNEL3x3[3]) * arrayOfPoints[0].B + (KERNEL3x3[1] + KERNEL3x3[4]) * arrayOfPoints[1].B + (KERNEL3x3[2] + KERNEL3x3[5]) * arrayOfPoints[2].B + KERNEL3x3[6] * arrayOfPoints[3].B + KERNEL3x3[7] * arrayOfPoints[4].B + KERNEL3x3[8] * arrayOfPoints[5].B; return ColorBgra.FromBgr(getSharpenedSubPixel(arrayOfPoints[1].B, b), getSharpenedSubPixel(arrayOfPoints[1].G, g), getSharpenedSubPixel(arrayOfPoints[1].R, r)); } private void copyBorderLeft(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x, y - 1]; dest[1] = src[x, y]; dest[2] = src[x, y + 1]; dest[3] = src[x + 1, y - 1]; dest[4] = src[x + 1, y]; dest[5] = src[x + 1, y + 1]; } private void copyBorderRight(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x, y - 1]; dest[1] = src[x, y]; dest[2] = src[x, y + 1]; dest[3] = src[x - 1, y - 1]; dest[4] = src[x - 1, y]; dest[5] = src[x - 1, y + 1]; } private void copyBorderTop(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x - 1, y]; dest[1] = src[x, y]; dest[2] = src[x + 1, y]; dest[3] = src[x - 1, y + 1]; dest[4] = src[x, y + 1]; dest[5] = src[x + 1, y + 1]; } private void copyBorderBottom(Surface src, int x, int y, ColorBgra[] dest) { dest[0] = src[x - 1, y]; dest[1] = src[x, y]; dest[2] = src[x + 1, y]; dest[3] = src[x - 1, y - 1]; dest[4] = src[x, y - 1]; dest[5] = src[x + 1, y - 1]; } private ColorBgra convolve(Surface src, int x, int y, byte radius) { short[] kernel = KERNEL_ARRAY[radius]; int r = 0; int g = 0; int b = 0; int offset = 0; for (int j = y - radius; j <= y + radius; j++) { for (int i = x - radius; i <= x + radius; i++) { ColorBgra srcPixel = src[i, j]; r = r + kernel[offset] * srcPixel.R; g = g + kernel[offset] * srcPixel.G; b = b + kernel[offset] * srcPixel.B; offset = offset + 1; } } return ColorBgra.FromBgr(getSharpenedSubPixel(src[x, y].B, b), getSharpenedSubPixel(src[x, y].G, g), getSharpenedSubPixel(src[x, y].R, r)); } private ColorBgra processPixel(Surface src, int x, int y, Rectangle bounds, ColorBgra[] arrayOfPoints) { if (((x == bounds.Left) || (x == bounds.Right - 1)) && ((y == bounds.Top) || (y == bounds.Bottom - 1))) { // Extreme corner => identity return src[x, y]; } if (x == bounds.Left) { copyBorderLeft(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (x == bounds.Right - 1) { copyBorderRight(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (y == bounds.Top) { copyBorderTop(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if (y == bounds.Bottom - 1) { copyBorderBottom(src, x, y, arrayOfPoints); return convolveBorder(arrayOfPoints); } if ((x == bounds.Left + 1) || (x == bounds.Right - 2) || (y == bounds.Top + 1) || (y == bounds.Bottom - 2)) { return convolve(src, x, y, 1); } if ((x == bounds.Left + 2) || (x == bounds.Right - 3) || (y == bounds.Top + 2) || (y == bounds.Bottom - 3)) { return convolve(src, x, y, 2); } return convolve(src, x, y, 3); } void PreRender(Surface dst, Surface src) { // 10 => 32 for fast division strength = (byte) (3 * sliderValue + 2); } void Render(Surface dst, Surface src, Rectangle rect) { ColorBgra[] borderPixels = new ColorBgra[6]; for (int y = rect.Top; y < rect.Bottom; y++) { if (IsCancelRequested) return; for (int x = rect.Left; x < rect.Right; x++) { dst[x, y] = processPixel(src, x, y, rect, borderPixels); } } }
  15. Plugin Ordered Dither v1.3 I change the menu location, I use your proposed icon and the if statement is replaced by a switch case. // Name: Ordered dither // Submenu: Stylize // Author: Pascal Ollive // Title: Ordered dither // Version: 1.3 // Desc: Color reduction with dither // Keywords: ordered|dither|color|reduction // URL: // Help: #region UICode RadioButtonControl ditherMethod = 0; // Dither method|Checkerboard|Dispersed|Arcade|Ordered|Lines|Custom|Random CheckboxControl isMonochrom = false; // Monochrom IntSliderControl PaletteSize = 2; // [2,8] Palette #endregion private int colorScale = 255; private byte[] ditherMatrix = null; private static byte[] createDitherMatrix(byte ditherMethod) { if ((ditherMethod == 3) || (ditherMethod == 4)) { // Ordered // Lines return new byte[] { 8, 9, 6, 7, 1, 0, 3, 2, 4, 5, 9, 8, 2, 3, 0, 1, 4, 5, 7, 6, 1, 0, 3, 2, 4, 5, 8, 9, 6, 7, 0, 1, 4, 5, 7, 6, 9, 8, 2, 3, 4, 5, 8, 9, 6, 7, 1, 0, 3, 2, 7, 6, 9, 8, 2, 3, 0, 1, 4, 5, 6, 7, 1, 0, 3, 2, 4, 5, 8, 9, 2, 3, 0, 1, 4, 5, 7, 6, 9, 8, 3, 2, 4, 5, 8, 9, 6, 7, 1, 0, 4, 5, 7, 6, 9, 8, 2, 3, 0, 1 }; } if (ditherMethod == 5) { // Custom return new byte[] { 133, 43, 161, 232, 138, 109, 41, 247, 97, 217, 118, 204, 103, 62, 224, 108, 15, 238, 86, 61, 24, 201, 158, 5, 173, 32, 154, 16, 135, 166, 36, 182, 149, 200, 125, 171, 245, 71, 100, 223, 130, 68, 244, 54, 196, 83, 246, 70, 99, 55, 4, 212, 35, 140, 179, 47, 89, 209, 180, 96, 231, 3, 123, 211, 25, 242, 153, 95, 116, 194, 17, 248, 152, 13, 120, 37, 139, 177, 45, 156, 110, 184, 75, 218, 53, 236, 74, 107, 190, 57, 234, 164, 67, 216, 88, 228, 60, 132, 31, 170, 11, 159, 131, 38, 219, 137, 102, 9, 195, 112, 20, 191, 6, 249, 198, 117, 230, 91, 213, 167, 22, 72, 176, 250, 142, 52, 239, 148, 175, 98, 48, 80, 146, 34, 63, 115, 241, 199, 39, 87, 29, 207, 124, 76, 225, 134, 210, 18, 172, 251, 193, 2, 92, 151, 122, 226, 160, 93, 183, 40, 23, 162, 59, 233, 111, 78, 129, 163, 222, 56, 14, 188, 64, 8, 214, 113, 243, 90, 127, 186, 12, 206, 51, 33, 197, 85, 252, 136, 104, 240, 141, 69, 181, 1, 220, 42, 94, 155, 237, 143, 106, 174, 30, 208, 44, 165, 26, 202, 105, 147, 77, 169, 253, 119, 73, 7, 229, 66, 150, 114, 79, 227, 121, 49, 254, 28, 215, 128, 58, 27, 178, 203, 126, 19, 235, 168, 0, 189, 84, 157, 65, 192, 101, 10, 187, 221, 82, 145, 50, 185, 81, 46, 255, 144, 21, 205 }; } return null; } private uint reverse(uint n) { // Hacker's Delight, Figure 7-1 n = (n & 0x55555555) << 1 | (n >> 1) & 0x55555555; n = (n & 0x33333333) << 2 | (n >> 2) & 0x33333333; n = (n & 0x0f0f0f0f) << 4 | (n >> 4) & 0x0f0f0f0f; n = (n << 24) | ((n & 0xff00) << 8) | ((n >> 8) & 0xff00) | (n >> 24); return n; } // Luma(pixel) = 0.2126R + 0.7152G + 0.0722B // Coefficients are scaled into "9-bit" integer // The coefficient sum is equals to 513 // Luminosity is ranged between 0 (inclusive) and 255.5 (exclusive) // Quick dither to deliver 8-bit value => Binary pattern is fast and "good enough" private byte rgbToGray(byte r, byte g, byte b, byte binaryPattern) { return (byte) ((109 * r + 367 * g + 37 * b + 256 * binaryPattern) >> 9); } private int applyOrderedDither(byte luma8bit, int coefCount, int ditherPattern) { int pseudoColorCount = (PaletteSize - 1) * coefCount + 1; return (pseudoColorCount * luma8bit + 255 * ditherPattern - 1) / (255 * coefCount); } private int applyRandomDither(byte luma8bit, int x, int y) { uint seed = ((reverse((uint) y) >> 23) << 22 | reverse((uint) x) >> 10); // multiplicative congruential generator uint ditherPattern = (48271 * seed) % 2147483647; return (int) (((PaletteSize - 1) * luma8bit + (ditherPattern / 8421506)) / 255); } private int applyDither(byte luma8bit, int x, int y) { switch (ditherMethod) { case 0: // Checkerboard return applyOrderedDither(luma8bit, 2, (x ^ y) & 0x1); case 1: // Dispersed return applyOrderedDither(luma8bit, 4, (((x ^ y) & 0x1) << 1) | (y & 0x1)); case 2: // Arcade return applyOrderedDither(luma8bit, 8, 2 + (y & 0x3)); case 3: // Ordered return applyOrderedDither(luma8bit, 5, ditherMatrix[20 * (y % 5) + 2 * (x % 5)] >> 1); case 4: // Lines return applyOrderedDither(luma8bit, 10, ditherMatrix[10 * (y % 10) + (x % 10)]); case 5: // Custom return applyOrderedDither(luma8bit, 256, ditherMatrix[((y & 0xf) << 4) + (x & 0xf)]); default: return applyRandomDither(luma8bit, x, y); } } void PreRender(Surface dst, Surface src) { colorScale = 255 / (PaletteSize - 1); ditherMatrix = createDitherMatrix(ditherMethod); } private ColorBgra processPixel(ColorBgra currentPixel, int x, int y) { if (isMonochrom) { byte luma8bit = rgbToGray(currentPixel.R, currentPixel.G, currentPixel.B, (byte) ((x ^ y) & 0x1)); luma8bit = (byte) applyDither(luma8bit, x, y); return ColorBgra.FromBgr(luma8bit, luma8bit, luma8bit); } return ColorBgra.FromBgr((byte) applyDither(currentPixel.B, x, y), (byte) applyDither(currentPixel.G, x, y), (byte) applyDither(currentPixel.R, x, y)); } void Render(Surface dst, Surface src, Rectangle rect) { for (int y = rect.Top; y < rect.Bottom; y++) { if (IsCancelRequested) return; for (int x = rect.Left; x < rect.Right; x++) { ColorBgra pixel = processPixel(src[x, y], x, y); pixel.R = (byte) (colorScale * pixel.R); pixel.G = (byte) (colorScale * pixel.G); pixel.B = (byte) (colorScale * pixel.B); dst[x, y] = pixel; } } } OrderedDither.zip