Jump to content

loupasc

Members
  • Posts

    31
  • Joined

  • Last visited

  • Days Won

    1

Posts posted by loupasc

  1. Version 1.6.7489.24864

     

    Minor improvements (kernel matrix accuracy and to8bit clamp function a bit more efficient).
     

    Spoiler

     

    
    // Name: Slight edge boost
    // Submenu: Photo
    // Author: Pascal Ollive
    // Title: Slight edge boost
    // Version: 1.6
    // Desc: Gaussian unsharp mask
    // Keywords: Sharpening|Gaussian|filter
    // URL: https://forums.getpaint.net/profile/160055-loupasc/
    // Help:
    #region UICode
    RadioButtonControl colorSpace = 0; // Select sharpen target|RGB (all)|YUV (Y only)
    IntSliderControl sliderValue = 5; // [0,10] Strength
    #endregion
    
    // Scale double -> integer
    private const int KERNEL_COEF_SCALE = -22306;
    
    // 10 amounts depending of sliderValue
    private static byte[] AMOUNT = {2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233};
    
    // Significant value in 9x9 border
    private static int EXTREME_BORDER_COEF = (int) Math.Round(KERNEL_COEF_SCALE * gaussian(0, -8 / 3.0));
    
    // 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 = createKernelMatrix();
    
    private static double gaussian(double x, double y) {
        double sqr = x * x + y * y;
        return 2 * Math.Exp(-6 * sqr) + Math.Exp(-9 * sqr / 8);
    }
    
    private static int[,] createKernelMatrix() {
        int[,] matrix = new int[7, 7];
        int sum = 4 * EXTREME_BORDER_COEF;
        for (int j = 0; j < 7; j++) {
            for (int i = 0; i < 7; i++) {
                // support 2 => 1.5
                matrix[i, j] = (int) Math.Round(KERNEL_COEF_SCALE * gaussian(2 * (i - 3)/ 3.0, 2 * (j - 3)/ 3.0));
                sum = sum + matrix[i, j];
            }
        }
        //
        // sum(matrix) = 0
        matrix[3, 3] = matrix[3, 3] - sum;
        return matrix;
    }
    
    private byte strength = 21; // [2,233]
    
    // ---------------------------
    // -- Color space functions --
    // ---------------------------
    
    // -- RGB to YUV --
    
    // out [0 .. 255]
    private static double bgrToY(ColorBgra c) {
        return 0.299 * c.R + 0.587 * c.G + 0.114 * c.B;
    }
    
    // out [-111.18 .. 111.18]
    private static double bgrToU(ColorBgra c) {
        return -0.14714 * c.R - 0.28886 * c.G + 0.436 * c.B;
    }
    
    // out [-156.825 .. 156.825]
    private static double bgrToV(ColorBgra c) {
        return 0.615 * c.R - 0.51499 * c.G - 0.10001 * c.B;
    }
    
    // -- YUV to RGB --
    
    private static byte to8bit(double x) {
        if (x < 0) {
            return 0;
        }
        if (x > 255) {
            return 255;
        }
        return (byte) Math.Round(x);
    }
    
    private static ColorBgra yuvToBgr(double y, double u, double v) {
        byte b = to8bit(y + 2.032078 * u); // 0.299/0.14714
        byte g = to8bit(y - 0.39465 * u - 0.5806 * v);
        byte r = to8bit(y + 1.139827 * v); // 0.587/0.51499
    
        return ColorBgra.FromBgr(b, g, r);
    }
    
    //
    // ---------------------------
    //
    
    private static int surfaceExtend(int x, int maxValue) {
        if (x < 0) {
            return 0;
        }
        if (x < maxValue) {
            return x;
        }
        return maxValue - 1;
    }
    
    // Safe access to Color BGRA surface
    private static ColorBgra getSafeSurfacePixel(Surface s, int x, int y) {
        if ((x >= 0) && (y >= 0) && (x < s.Width) && (y < s.Height)) {
            return s[x, y];
        }
        return s[surfaceExtend(x, s.Width), surfaceExtend(y, s.Height)];
    }
    
    // Utility function to provide result in 8-bit range
    // strength 2^5
    // factor 2^16
    // 21-bit scale
    private static byte to8bit(long n) {
        if (n > 533725183) {
            return 255;
        }
        if (n < 0) {
            return 0;
        }
        return (byte) ((n + 1048576) >> 21);
    }
    
    private static int[] convolveRgbBorder(Surface src, int x, int y) {
        int[] rgb = { 0, 0, 0 };
        ColorBgra c1 = getSafeSurfacePixel(src, x, y - 4);
        ColorBgra c2 = getSafeSurfacePixel(src, x - 4, y);
        ColorBgra c3 = getSafeSurfacePixel(src, x + 4, y);
        ColorBgra c4 = getSafeSurfacePixel(src, x, y + 4);
    
        rgb[0] = EXTREME_BORDER_COEF * (c1.R + c2.R + c3.R + c4.R);
        rgb[1] = EXTREME_BORDER_COEF * (c1.G + c2.G + c3.G + c4.G);
        rgb[2] = EXTREME_BORDER_COEF * (c1.B + c2.B + c3.B + c4.B);
    
        return rgb;
    }
    
    private ColorBgra convolveRGB(Surface src, int x, int y, bool isCentralArea) {
        int[] rgb = convolveRgbBorder(src, x, y);
        // strength 2^5 and factor 2^16 => 21-bit scale
        long r = (src[x, y].R << 21) + rgb[0] * strength;
        long g = (src[x, y].G << 21) + rgb[1] * strength;
        long b = (src[x, y].B << 21) + rgb[2] * strength;    
    
        for (int j = 0; j < 7; j++) {
            int posY = y + j - 3;
            for (int i = 0; i < 7; i++) {
                int posX = x + i - 3;
                long coef = KERNEL7x7[i, j] * strength;
                ColorBgra srcPixel = (isCentralArea ? src[posX, posY]
                        : getSafeSurfacePixel(src, posX, posY));
                r = r + coef * srcPixel.R;
                g = g + coef * srcPixel.G;
                b = b + coef * srcPixel.B;
            }
        }
    
        return ColorBgra.FromBgr(to8bit(b), to8bit(g), to8bit(r));
    }
    
    private static double convolveYuvBorder(Surface src, int x, int y) {
        return EXTREME_BORDER_COEF * (bgrToY(getSafeSurfacePixel(src, x, y - 4))
                + bgrToY(getSafeSurfacePixel(src, x - 4, y))
                + bgrToY(getSafeSurfacePixel(src, x + 4, y))
                + bgrToY(getSafeSurfacePixel(src, x, y + 4)));
    }
    
    private ColorBgra convolveYUV(Surface src, int x, int y, bool isCentralArea) {
        // strength 2^5 and factor 2^16 => 2^21 (2097152)
        double totalLuma = bgrToY(src[x, y]) * 2097152.0;
        double u = bgrToU(src[x, y]);
        double v = bgrToV(src[x, y]);
    
        totalLuma = totalLuma + convolveYuvBorder(src, x, y) * strength;
    
        for (int j = 0; j < 7; j++) {
            int posY = y + j - 3;
            for (int i = 0; i < 7; i++) {
                int posX = x + i - 3;
                double luma = (isCentralArea ? bgrToY(src[posX, posY])
                        : bgrToY(getSafeSurfacePixel(src, posX, posY)));
                totalLuma = totalLuma + luma * KERNEL7x7[i, j] * strength;
            }
        }
    
        return yuvToBgr(totalLuma / 2097152.0, u, v);
    }
    
    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) {
        bool isCentralArea = ((rect.Left >= 3) && (rect.Top >= 3)
                && (rect.Right <= src.Width - 3) && (rect.Bottom <= src.Height - 3));
    
        for (int y = rect.Top; y < rect.Bottom; y++) {
            if (IsCancelRequested) return;
            for (int x = rect.Left; x < rect.Right; x++) {
                dst[x, y] = (colorSpace == 0 ? convolveRGB(src, x, y, isCentralArea)
                        : convolveYUV(src, x, y, isCentralArea));
            }
        }
    }

     

     

    • Like 3
  2. Version 1.5.7467.22643

     

    Source

    Spoiler
    
    // Name: Slight edge boost
    // Submenu: Photo
    // Author: Pascal Ollive
    // Title: Slight edge boost
    // Version: 1.5
    // Desc: Gaussian unsharp mask
    // Keywords: Sharpening|Gaussian|filter
    // URL: https://forums.getpaint.net/profile/160055-loupasc/
    // Help:
    #region UICode
    RadioButtonControl colorSpace = 0; // Select sharpen target|RGB (all)|YUV (Y only)
    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 = createKernelMatrix();
    
    private static double gaussian(double x, double y) {
        double sqr = x * x + y * y;
        return 2 * Math.Exp(-6 * sqr) + Math.Exp(-9 * sqr / 8);
    }
    
    private static int[,] createKernelMatrix() {
        int[,] matrix = new int[7, 7];
        for (int j = 0; j < 7; j++) {
            for (int i = 0; i < 7; i++) {
                // support 2 => 1.5
                matrix[i, j] = (int) Math.Round(-22311.3 * gaussian(2 * (i - 3)/ 3.0, 2 * (j - 3)/ 3.0));
            }
        }
        //
        // sum(matrix) = 0
        matrix[3, 3] = 131068;
        return matrix;
    }
    
    private byte strength = 21; // [2,233]
    
    // ---------------------------
    // -- Color space functions --
    // ---------------------------
    
    // -- RGB to YUV --
    
    // out [0 .. 255]
    private static double bgrToY(ColorBgra c) {
        return 0.299 * c.R + 0.587 * c.G + 0.114 * c.B;
    }
    
    // out [-111.18 .. 111.18]
    private static double bgrToU(ColorBgra c) {
        return -0.14714 * c.R - 0.28886 * c.G + 0.436 * c.B;
    }
    
    // out [-156.825 .. 156.825]
    private static double bgrToV(ColorBgra c) {
        return 0.615 * c.R - 0.51499 * c.G - 0.10001 * c.B;
    }
    
    // -- YUV to RGB --
    
    private static byte to8bit(double x) {
        if (x < 0) {
            return 0;
        }
        if (x > 255) {
            return 255;
        }
        return (byte) Math.Round(x);
    }
    
    private static ColorBgra yuvToBgr(double y, double u, double v) {
        byte b = to8bit(y + 2.032078 * u); // 0.299/0.14714
        byte g = to8bit(y - 0.39465 * u - 0.5806 * v);
        byte r = to8bit(y + 1.139827 * v); // 0.587/0.51499
    
        return ColorBgra.FromBgr(b, g, r);
    }
    
    //
    // ---------------------------
    //
    
    private static int surfaceExtend(int x, int maxValue) {
        if (x < 0) {
            return 0;
        }
        if (x < maxValue) {
            return x;
        }
        return maxValue - 1;
    }
    
    // Safe access to Color BGRA surface
    private static ColorBgra getSafeSurfacePixel(Surface s, int x, int y) {
        if ((x >= 0) && (y >= 0) && (x < s.Width) && (y < s.Height)) {
            return s[x, y];
        }
        return s[surfaceExtend(x, s.Width), surfaceExtend(y, s.Height)];
    }
    
    // Utility function to provide result in 8-bit range
    // strength 2^5
    // factor 2^16
    // 21-bit scale
    private static byte to8bit(long n) {
        if (n > 535822335) {
            return 255;
        }
        if (n < 0) {
            return 0;
        }
        return (byte) ((n + 1048576) >> 21);
    }
    
    private ColorBgra convolveRGB(Surface src, int x, int y, bool isCentralArea) {
        // strength 2^5 and factor 2^16 => 21-bit scale
        long r = src[x, y].R << 21;
        long g = src[x, y].G << 21;
        long b = src[x, y].B << 21;
    
        for (int j = 0; j < 7; j++) {
            int posY = y + j - 3;
            for (int i = 0; i < 7; i++) {
                int posX = x + i - 3;
                long coef = KERNEL7x7[i, j] * strength;
                ColorBgra srcPixel = (isCentralArea ? src[posX, posY]
                        : getSafeSurfacePixel(src, posX, posY));
                r = r + coef * srcPixel.R;
                g = g + coef * srcPixel.G;
                b = b + coef * srcPixel.B;
            }
        }
    
        return ColorBgra.FromBgr(to8bit(b), to8bit(g), to8bit(r));
    }
    
    private ColorBgra convolveYUV(Surface src, int x, int y, bool isCentralArea) {
        // strength 2^5 and factor 2^16 => 2^21 (2097152)
        double totalLuma = bgrToY(src[x, y]) * 2097152.0;
        double u = bgrToU(src[x, y]);
        double v = bgrToV(src[x, y]);
    
        for (int j = 0; j < 7; j++) {
            int posY = y + j - 3;
            for (int i = 0; i < 7; i++) {
                int posX = x + i - 3;
                double luma = (isCentralArea ? bgrToY(src[posX, posY])
                        : bgrToY(getSafeSurfacePixel(src, posX, posY)));
                totalLuma = totalLuma + luma * KERNEL7x7[i, j] * strength;
            }
        }
    
        return yuvToBgr(totalLuma / 2097152.0, u, v);
    }
    
    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) {
        bool isCentralArea = ((rect.Left >= 3) && (rect.Top >= 3)
                && (rect.Right <= src.Width - 3) && (rect.Bottom <= src.Height - 3));
    
        for (int y = rect.Top; y < rect.Bottom; y++) {
            if (IsCancelRequested) return;
            for (int x = rect.Left; x < rect.Right; x++) {
                dst[x, y] = (colorSpace == 0 ? convolveRGB(src, x, y, isCentralArea)
                        : convolveYUV(src, x, y, isCentralArea));
            }
        }
    }

     

     

    • Like 1
  3. Slight edge boost

     

    Last version 1.6.7489.24864 [ 06 July 2020 ]

     

    Download link > > >  Download

     

    Location: Effects -> Photo

    Corresponding source is available in the last post.

     

    Plugin description

    interface.png.e6edd00371b35b359e3efe85ca8c1c00.png

     

    This plugin enhances image edges using unsharp mask method.

    It provides the possibility to apply sharpen only to the image luminosity in order to avoid chromatic noise amplification.

     

    Illustration on a stenope (blurry) photography

    Spoiler

    edgeBoost_demo.gif

     

     

    • Like 1
    • Upvote 2
  4. Version 1.5.7466.17310

     

    Fix crash with small image using mirror border handling.
    Improve accuracy of the matrix coefficients ( Math.Sin( k * Pi ) = 0).

     

    Source

    Spoiler
    
    // Name: WaveToolBox
    // Submenu: Advanced
    // Author: Pascal Ollive
    // Title: Wave tool box
    // Version: 1.5
    // Desc: Reconstruction filter tool box
    // Keywords: convolution|kernel|filter
    // URL: https://forums.getpaint.net/profile/160055-loupasc/
    // Help:
    #region UICode
    RadioButtonControl kernelShape = 0; // Shape|Square|Disc|Diamond
    IntSliderControl cutoffFactor = 2; // [1,4] Cutoff sharpness
    IntSliderControl radiusSliderIndex = 8; // [8,100] Radius
    RadioButtonControl borderHandling = 2; // Border handling|Uniform|Extend|Mirror
    IntSliderControl contrastSliderIndex = 5; // [0,10] Contrast
    #endregion
    
    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 lanczos(int n, int lobeCount, byte radius) {
        if (n == 0) {
            return 1.0;
        }
        if ((lobeCount * n) % radius == 0) {
            return 0.0;
        }
        // normalize into lanczos window
        return lanczos((double) lobeCount * n / radius, lobeCount);
    }
    
    private static double lanczos(int i, int j, int lobeCount, byte radius) {
        if (i == 0) {
            // lanczos(i, lobeCount, radius) = 1
            return lanczos(j, lobeCount, radius);
        }
        if (j == 0) {
            // lanczos(j, lobeCount, radius) = 1
            return lanczos(i, lobeCount, radius);
        }
        // i and j are not equal to zero
        if (((lobeCount * i) % radius == 0) || ((lobeCount * j) % radius == 0)) {
            return 0.0;
        }
    
        return lanczos(i, lobeCount, radius) * lanczos(j, lobeCount, radius);
    }
    
    // Returns the matrix coefficient at (i,j) in function of kernel properties
    // - shape - number of lobes - radius -
    private static double computeLanczosCoef(int i, int j, byte shape, int lobeCount, byte radius) {
        if (shape == 0) {
            // Square
            return lanczos(i, j, lobeCount, radius);
        }
    
        if (shape == 1) {
            // Disc
            if ((i == 0) || (j == 0)) {
                return lanczos(i, j, lobeCount, radius);
            }
            double d = Math.Sqrt(i * i + j * j);
            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)
            int p = (i + j);
            if (p < radius) {
                return lanczos(p, i - j, lobeCount, radius);
            }
        }
    
        return 0.0;
    }
    
    // Matrix content has symmetries on horizontal axis and vertical axis
    // => Only a quarter of the matrix is generated
    // => Possibility to factorize computation allowing to divide number of multiplications by 4
    private static double[,] createLanczosKernel(byte shape, int lobeCount, byte radius) {
        double[,] matrix = new double[radius, radius];
    
        for (int j = 0; j < radius; j++) {
            for (int i = 0; i < radius; i++) {
                matrix[i, j] = computeLanczosCoef(i, j, shape, lobeCount, radius);
            }
        }
    
        return matrix;
    }
    
    private static double getCoefficientSum(double[,] m) {
        // Assuming dealing with square matrix (GetLength(0) == GetLength(1))
        byte radius = (byte) m.GetLength(0);
        // Let's start with the center coefficient
        double result = m[0, 0];
    
        // Next loop in the central axis
        for (int i = 1; i < radius; i++) {
            result = result + 4 * m[i, 0];
        }
    
        // Finally the remaining area
        for (int j = 1; j < radius; j++) {
            for (int i = 1; i < radius; i++) {
                result = result + 4 * m[i, j];
            }
        }
    
        return result;
    }
    
    private static int surfaceExtend(int x, int maxValue) {
        if (x < 0) {
            return 0;
        }
        if (x < maxValue) {
            return x;
        }
        return maxValue - 1;
    }
    
    private static int surfaceMirror(int x, int maxValue) {
        int symX;
    
        if (x < 0) {
            symX = - x;
            if (symX < maxValue) {
                return symX;
            }
            return maxValue - 1;
        }
        if (x < maxValue) {
            return x;
        }
        symX = 2 * maxValue - x - 1;
        if (symX < 0) {
            return 0;
        }
        return symX;
    }
    
    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), surfaceExtend(y, s.Height)];
            }
            // Mirror
            return s[surfaceMirror(x, s.Width), surfaceMirror(y, s.Height)];
        }
        // Background color
        return ColorBgra.Black;
    }
    
    private double[] convolve(Surface s, int x, int y, bool isCentralArea, double[,] m, byte radius) {
        // Let's start with the center coefficient
        double[] bgr = { s[x, y].B * m[0, 0], s[x, y].G * m[0, 0], s[x, y].R * m[0, 0]};
    
        // Next loop in the central axis
        for (int i = 1; i < radius; i++) {
            double coef = m[i, 0];
            ColorBgra c1;
            ColorBgra c2;
            ColorBgra c3;
            ColorBgra c4;
            if (isCentralArea) {
                c1 = s[x - i, y];
                c2 = s[x + i, y];
                c3 = s[x, y - i];
                c4 = s[x, y + i];
            }
            else {
                c1 = getSafeSurfacePixel(s, x - i, y);
                c2 = getSafeSurfacePixel(s, x + i, y);
                c3 = getSafeSurfacePixel(s, x, y - i);
                c4 = getSafeSurfacePixel(s, x, y + i);
            }
            bgr[0] = bgr[0] + coef * (c1.B + c2.B + c3.B + c4.B);
            bgr[1] = bgr[1] + coef * (c1.G + c2.G + c3.G + c4.G);
            bgr[2] = bgr[2] + coef * (c1.R + c2.R + c3.R + c4.R);
        }
    
        // Finally the remaining area
        for (int j = 1; j < radius; j++) {
            for (int i = 1; i < radius; i++) {
                double coef = m[i, j];
                ColorBgra c1;
                ColorBgra c2;
                ColorBgra c3;
                ColorBgra c4;
                if (isCentralArea) {
                    c1 = s[x - i, y - j];
                    c2 = s[x + i, y - j];
                    c3 = s[x - i, y + j];
                    c4 = s[x + i, y + j];
                }
                else {
                    c1 = getSafeSurfacePixel(s, x - i, y - j);
                    c2 = getSafeSurfacePixel(s, x + i, y - j);
                    c3 = getSafeSurfacePixel(s, x - i, y + j);
                    c4 = getSafeSurfacePixel(s, x + i, y + j);
                }
                bgr[0] = bgr[0] + coef * (c1.B + c2.B + c3.B + c4.B);
                bgr[1] = bgr[1] + coef * (c1.G + c2.G + c3.G + c4.G);
                bgr[2] = bgr[2] + coef * (c1.R + c2.R + c3.R + c4.R);
            }
        }
    
        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
        byte kernelRadius = (byte) ((radiusSliderIndex * cutoffFactor) / 2);
        kernelMatrix = createLanczosKernel(kernelShape, 4 * cutoffFactor, kernelRadius);
        kernelNorm = getCoefficientSum(kernelMatrix);
    }
    
    void Render(Surface dst, Surface src, Rectangle rect) {
        // Assuming dealing with square matrix (GetLength(0) == GetLength(1))
        byte radius = (byte) kernelMatrix.GetLength(0);
        bool isCentralArea = ((rect.Left >= radius) && (rect.Top >= radius) && (rect.Right <= src.Width - radius) && (rect.Bottom <= src.Height - radius));
    
        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, isCentralArea, kernelMatrix, radius);
                dst[x,y] = ColorBgra.FromBgr(to8bit(applyContrast(bgr[0] / kernelNorm)),
                        to8bit(applyContrast(bgr[1] / kernelNorm)),
                        to8bit(applyContrast(bgr[2] / kernelNorm)));
            }
        }
    }

     

     

  5. Version 1.4.7461.19568

     

    Optimization in the rendering method.
    Test if the rectangle boundary is far enough from the border allowing useless limit tests when getting surface color pixels.

    Spoiler
    
    // Name: WaveToolBox
    // Submenu: Advanced
    // Author: Pascal Ollive
    // Title: Wave tool box
    // Version: 1.4
    // Desc: Reconstruction filter tool box
    // Keywords: convolution|kernel|filter
    // URL: https://forums.getpaint.net/profile/160055-loupasc/
    // Help:
    #region UICode
    RadioButtonControl kernelShape = 0; // Shape|Square|Disc|Diamond
    IntSliderControl cutoffFactor = 2; // [1,4] Cutoff sharpness
    IntSliderControl radiusSliderIndex = 8; // [8,100] Radius
    RadioButtonControl borderHandling = 2; // Border handling|Uniform|Extend|Mirror
    IntSliderControl contrastSliderIndex = 5; // [0,10] Contrast
    #endregion
    
    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);
    }
    
    // Returns the matrix coefficient at (i,j) in function of kernel properties
    // - shape - number of lobes - radius -
    private static double computeLanczosCoef(int i, int j, byte shape, int lobeCount, byte radius) {
        if (shape == 0) {
            // Square
            // normalize into lanczos window
            return lanczos((double) lobeCount * i / radius, lobeCount) * lanczos((double) lobeCount * j / radius, lobeCount);
        }
    
        if (shape == 1) {
            // Disc
            double d = Math.Sqrt(i * i + j * j);
            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)
            double p = (i + j);
            if (p < radius) {
                // normalize into lanczos window
                return lanczos(lobeCount * p / radius, lobeCount) * lanczos(lobeCount * (p - 2 * j) / radius, lobeCount);
            }
        }    
    
        return 0.0;
    }
    
    // Matrix content has symmetries on horizontal axis and vertical axis
    // => Only a quarter of the matrix is generated
    // => Possibility to factorize computation allowing to divide number of multiplications by 4
    private static double[,] createLanczosKernel(byte shape, int lobeCount, byte radius) {
        double[,] matrix = new double[radius, radius];
    
        for (int j = 0; j < radius; j++) {
            for (int i = 0; i < radius; i++) {
                matrix[i, j] = computeLanczosCoef(i, j, shape, lobeCount, radius);
            }
        }
    
        return matrix;
    }
    
    private static double getCoefficientSum(double[,] m) {
        // Assuming dealing with square matrix (GetLength(0) == GetLength(1))
        byte radius = (byte) m.GetLength(0);
        // Let's start with the center coefficient
        double result = m[0, 0];
    
        // Next loop in the central axis
        for (int i = 1; i < radius; i++) {
            result = result + 4 * m[i, 0];
        }
    
        // Finally the remaining area
        for (int j = 1; j < radius; j++) {
            for (int i = 1; i < radius; i++) {
                result = result + 4 * m[i, j];
            }
        }
    
        return result;
    }
    
    private static int surfaceExtend(int x, int maxValue) {
         if (x < 0) {
             return 0;
         }
         if (x < maxValue) {
             return x;
         }
         return maxValue - 1;
    }
    
    private static int surfaceMirror(int x, int maxValue) {
        if (x < 0) {
             return - x;
         }
         if (x < maxValue) {
             return x;
         }
         return 2 * maxValue - x - 1;
    }
    
    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), surfaceExtend(y, s.Height)];
            }
            // Mirror
            return s[surfaceMirror(x, s.Width), surfaceMirror(y, s.Height)];
        }
        // Background color
        return ColorBgra.Black;
    }
    
    private double[] convolve(Surface s, int x, int y, bool isCentralArea, double[,] m, byte radius) {
        // Let's start with the center coefficient
        double[] bgr = { s[x, y].B * m[0, 0], s[x, y].G * m[0, 0], s[x, y].R * m[0, 0]};
    
        // Next loop in the central axis
        for (int i = 1; i < radius; i++) {
            double coef = m[i, 0];
            ColorBgra c1;
            ColorBgra c2;
            ColorBgra c3;
            ColorBgra c4;
            if (isCentralArea) {
                c1 = s[x - i, y];
                c2 = s[x + i, y];
                c3 = s[x, y - i];
                c4 = s[x, y + i];
            }
            else {
                c1 = getSafeSurfacePixel(s, x - i, y);
                c2 = getSafeSurfacePixel(s, x + i, y);
                c3 = getSafeSurfacePixel(s, x, y - i);
                c4 = getSafeSurfacePixel(s, x, y + i);
            }
            bgr[0] = bgr[0] + coef * (c1.B + c2.B + c3.B + c4.B);
            bgr[1] = bgr[1] + coef * (c1.G + c2.G + c3.G + c4.G);
            bgr[2] = bgr[2] + coef * (c1.R + c2.R + c3.R + c4.R);
        }
    
        // Finally the remaining area
        for (int j = 1; j < radius; j++) {
            for (int i = 1; i < radius; i++) {
                double coef = m[i, j];
                ColorBgra c1;
                ColorBgra c2;
                ColorBgra c3;
                ColorBgra c4;
                if (isCentralArea) {
                    c1 = s[x - i, y - j];
                    c2 = s[x + i, y - j];
                    c3 = s[x - i, y + j];
                    c4 = s[x + i, y + j];
                }
                else {
                    c1 = getSafeSurfacePixel(s, x - i, y - j);
                    c2 = getSafeSurfacePixel(s, x + i, y - j);
                    c3 = getSafeSurfacePixel(s, x - i, y + j);
                    c4 = getSafeSurfacePixel(s, x + i, y + j);
                }
                bgr[0] = bgr[0] + coef * (c1.B + c2.B + c3.B + c4.B);
                bgr[1] = bgr[1] + coef * (c1.G + c2.G + c3.G + c4.G);
                bgr[2] = bgr[2] + coef * (c1.R + c2.R + c3.R + c4.R);
            }
        }
    
        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
        byte kernelRadius = (byte) ((radiusSliderIndex * cutoffFactor) / 2);
        kernelMatrix = createLanczosKernel(kernelShape, 4 * cutoffFactor, kernelRadius);
        kernelNorm = getCoefficientSum(kernelMatrix);
    }
    
    void Render(Surface dst, Surface src, Rectangle rect) {
        // Assuming dealing with square matrix (GetLength(0) == GetLength(1))
        byte radius = (byte) kernelMatrix.GetLength(0);
        bool isCentralArea = ((rect.Left >= radius) && (rect.Top >= radius) && (rect.Right < src.Width - radius) && (rect.Bottom < src.Height - radius));
    
        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, isCentralArea, kernelMatrix, radius);
                dst[x,y] = ColorBgra.FromBgr(to8bit(applyContrast(bgr[0] / kernelNorm)),
                        to8bit(applyContrast(bgr[1] / kernelNorm)),
                        to8bit(applyContrast(bgr[2] / kernelNorm)));
            }
        }
    }

     

     

     

  6. Version 1.3.7456.16099

     

    Computation have been optimized. The kernel matrix has symmetries allowing to divide its storage by 4.

    Convolution has been optimized too the symmetry allows to factorize multiplications which divide the number of multiplication by four.

    Spoiler
    
    // Name: WaveToolBox
    // Submenu: Advanced
    // Author: Pascal Ollive
    // Title: Wave tool box
    // Version: 1.3
    // Desc: Reconstruction filter tool box
    // Keywords: convolution|kernel|filter
    // URL: https://forums.getpaint.net/profile/160055-loupasc/
    // Help:
    #region UICode
    RadioButtonControl kernelShape = 0; // Shape|Square|Disc|Diamond
    IntSliderControl cutoffFactor = 2; // [1,4] Cutoff sharpness
    IntSliderControl radiusSliderIndex = 8; // [8,100] Radius
    RadioButtonControl borderHandling = 2; // Border handling|Uniform|Extend|Mirror
    IntSliderControl contrastSliderIndex = 5; // [0,10] Contrast
    #endregion
    
    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);
    }
    
    // Returns the matrix coefficient at (i,j) in function of kernel properties
    // - shape - number of lobes - radius -
    private static double computeLanczosCoef(int i, int j, byte shape, int lobeCount, byte radius) {
        if (shape == 0) {
            // Square
            // normalize into lanczos window
            return lanczos((double) lobeCount * i / radius, lobeCount) * lanczos((double) lobeCount * j / radius, lobeCount);
        }
    
        if (shape == 1) {
            // Disc
            double d = Math.Sqrt(i * i + j * j);
            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)
            double p = (i + j);
            if (p < radius) {
                // normalize into lanczos window
                return lanczos(lobeCount * p / radius, lobeCount) * lanczos(lobeCount * (p - 2 * j) / radius, lobeCount);
            }
        }    
    
        return 0.0;
    }
    
    // Matrix content has symmetries on horizontal axis and vertical axis
    // => Only a quarter of the matrix is generated
    // => Possibility to factorize computation allowing to divide number of multiplications by 4
    private static double[,] createLanczosKernel(byte shape, int lobeCount, byte radius) {
        double[,] matrix = new double[radius, radius];
    
        for (int j = 0; j < radius; j++) {
            for (int i = 0; i < radius; i++) {
                matrix[i, j] = computeLanczosCoef(i, j, shape, lobeCount, radius);
            }
        }
    
        return matrix;
    }
    
    private static double getCoefficientSum(double[,] m) {
        // Assuming dealing with square matrix (GetLength(0) == GetLength(1))
        byte radius = (byte) m.GetLength(0);
        // Let's start with the center coefficient
        double result = m[0, 0];
    
        // Next loop in the central axis
        for (int i = 1; i < radius; i++) {
            result = result + 4 * m[i, 0];
        }
    
        // Finally the remaining area
        for (int j = 1; j < radius; j++) {
            for (int i = 1; i < radius; i++) {
                result = result + 4 * m[i, j];
            }
        }
    
        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) {
        // Assuming dealing with square matrix (GetLength(0) == GetLength(1))
        byte radius = (byte) m.GetLength(0);
    
        // Let's start with the center coefficient
        double[] bgr = { s[x, y].B * m[0, 0], s[x, y].G * m[0, 0], s[x, y].R * m[0, 0]};
    
        // Next loop in the central axis
        for (int i = 1; i < radius; i++) {
            double coef = m[i, 0];
            ColorBgra c1 = getSafeSurfacePixel(s, x - i, y);
            ColorBgra c2 = getSafeSurfacePixel(s, x + i, y);
            ColorBgra c3 = getSafeSurfacePixel(s, x, y - i);
            ColorBgra c4 = getSafeSurfacePixel(s, x, y + i);
            bgr[0] = bgr[0] + coef * (c1.B + c2.B + c3.B + c4.B);
            bgr[1] = bgr[1] + coef * (c1.G + c2.G + c3.G + c4.G);
            bgr[2] = bgr[2] + coef * (c1.R + c2.R + c3.R + c4.R);
        }
    
        // Finally the remaining area
        for (int j = 1; j < radius; j++) {
            for (int i = 1; i < radius; i++) {
                double coef = m[i, j];
                ColorBgra c1 = getSafeSurfacePixel(s, x - i, y - j);
                ColorBgra c2 = getSafeSurfacePixel(s, x + i, y - j);
                ColorBgra c3 = getSafeSurfacePixel(s, x - i, y + j);
                ColorBgra c4 = getSafeSurfacePixel(s, x + i, y + j);
                bgr[0] = bgr[0] + coef * (c1.B + c2.B + c3.B + c4.B);
                bgr[1] = bgr[1] + coef * (c1.G + c2.G + c3.G + c4.G);
                bgr[2] = bgr[2] + coef * (c1.R + c2.R + c3.R + c4.R);
            }
        }
    
        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
        byte kernelRadius = (byte) ((radiusSliderIndex * 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);
                dst[x,y] = ColorBgra.FromBgr(to8bit(applyContrast(bgr[0] / kernelNorm)),
                        to8bit(applyContrast(bgr[1] / kernelNorm)),
                        to8bit(applyContrast(bgr[2] / kernelNorm)));
            }
        }
    }

     

     

     

  7. 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).

    Spoiler
    
    // 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

    • Like 1
  8. 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 :) )

  9. WaveToolBox

     

    Last version 1.5.7466.17310 [ 10 June 2020 ]

     

    Download link > > >  Download

     

    Change log summary:

    • Fix crash with small image using mirror border handling
    • Improve matrix coefficient accuracy

    Location: Effects -> Advanced.

    Corresponding source is available in the last post.

     

    Plugin description

     

    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; optimization is in progress...

     

    Graphic user interface

    Spoiler

    interface.png.996bb72b3be764844392bf8cba

     

    Plugin usage

    - Inverse dithering

    - Anti-aliasing

     

    Spoiler

    waveToolBox_demo.gif

     

    And the corresponding spectrum (originally for validation purpose but I found it is nice to show)

    Spoiler

    waveToolBox_spectrum.gif

     

     

    Best regards,

     

    interface.png

     

    WaveToolBox.zip

    • Like 3
    • Upvote 1
  10. 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,

  11. 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

     

    waveToolBox_demo.gif

     

    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

    • Upvote 1
  12. 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

    • Like 1
    • Upvote 1
  13. 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.

     

  14. On 5/17/2020 at 5:52 AM, Ego Eram Reputo said:

    Submenu: Stylize

     

    ...and an icon :)

     

    image.png

     

     

     

    I'm interested where these weighting values came from? I've been using this intensity or luminescense formula forever....


        (iColor.R * 0.299) + (iColor.G * 0.587) + (iColor.B * 0.114)

     

    Edit: seems to make very little difference to the result.

     

    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

     

     

     

     

  15. 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:

     

    ChromaCube-Blockified.bmp

     

    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

    • Like 3
    • Upvote 1
  16. 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);
            }
        }
    }

     

     

     

  17. 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);
            }
        }
    }

     

     

  18. 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);
            }
        }
    }

     

    • Like 1
  19. 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);
            }
        }
    }

     

    SlightEdgeBoost.sample.png

    result.png

     

    • Like 1
    • Upvote 1
  20. 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

    • Like 1
    • Upvote 1
×
×
  • Create New...