All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.tommyettinger.anim8.FastPalette Maven / Gradle / Ivy

There is a newer version: 0.4.5
Show newest version
/*
 * Copyright (c) 2023  Tommy Ettinger
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *
 */

package com.github.tommyettinger.anim8;

import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.IntIntMap;

import java.nio.ByteBuffer;
import java.util.Arrays;

/**
 * A GWT-incompatible, optimized replacement for {@link PaletteReducer}.
 * This reads pixels byte-by-byte from a Pixmap's {@link Pixmap#getPixels()} buffer, rather than relying on
 * {@link Pixmap#getPixel(int, int)} to form RGBA8888 ints for every pixel we read.
 */
public class FastPalette extends PaletteReducer {
    private final transient byte[] workspace = new byte[0x8000];
    /**
     * Constructs a default FastPalette that uses the "Aurora" 255-color-plus-transparent palette.
     * Note that this uses a more-detailed and higher-quality metric than you would get by just specifying
     * {@code new FastPalette(FastPalette.AURORA)}; this metric would be too slow to calculate at
     * runtime, but as pre-calculated data it works very well.
     */
    public FastPalette() {
        super();
    }

    /**
     * Constructs a FastPalette that uses the given array of RGBA8888 ints as a palette (see {@link #exact(int[])}
     * for more info).
     *
     * @param rgbaPalette an array of RGBA8888 ints to use as a palette
     */
    public FastPalette(int[] rgbaPalette) {
        super(rgbaPalette);
    }

    /**
     * Constructs a FastPalette that uses the given array of RGBA8888 ints as a palette (see
     * {@link #exact(int[], int)} for more info).
     *
     * @param rgbaPalette an array of RGBA8888 ints to use as a palette
     * @param limit       how many int items to use from rgbaPalette (this always starts at index 0)
     */
    public FastPalette(int[] rgbaPalette, int limit) {
        super(rgbaPalette, limit);
    }

    /**
     * Constructs a FastPalette that uses the given array of Color objects as a palette (see {@link #exact(Color[])}
     * for more info).
     *
     * @param colorPalette an array of Color objects to use as a palette
     */
    public FastPalette(Color[] colorPalette) {
        super(colorPalette);
    }

    /**
     * Constructs a FastPalette that uses the given array of Color objects as a palette (see
     * {@link #exact(Color[], int)} for more info).
     *
     * @param colorPalette an array of Color objects to use as a palette
     * @param limit
     */
    public FastPalette(Color[] colorPalette, int limit) {
        super(colorPalette, limit);
    }

    /**
     * Constructs a FastPalette that analyzes the given Pixmap for color count and frequency to generate a palette
     * (see {@link #analyze(Pixmap)} for more info).
     *
     * @param pixmap a Pixmap to analyze in detail to produce a palette
     */
    public FastPalette(Pixmap pixmap) {
        super(pixmap);
    }

    /**
     * Constructs a FastPalette that analyzes the given Pixmaps for color count and frequency to generate a palette
     * (see {@link #analyze(Array)} for more info).
     *
     * @param pixmaps an Array of Pixmap to analyze in detail to produce a palette
     */
    public FastPalette(Array pixmaps) {
        super(pixmaps);
    }

    /**
     * Constructs a FastPalette that uses the given array of RGBA8888 ints as a palette (see
     * {@link #exact(int[], byte[])} for more info) and an encoded byte array to use to look up pre-loaded color data.
     * You can use {@link FastPalette#writePreloadFile(FileHandle)} to write the preload data for a given FastPalette, and
     * {@link #loadPreloadFile(FileHandle)} to get a byte array of preload data from a previously-written file.
     *
     * @param palette an array of RGBA8888 ints to use as a palette
     * @param preload a byte array containing preload data
     */
    public FastPalette(int[] palette, byte[] preload) {
        super(palette, preload);
    }

    /**
     * Constructs a FastPalette that analyzes the given Pixmap for color count and frequency to generate a palette
     * (see {@link #analyze(Pixmap, double)} for more info).
     *
     * @param pixmap    a Pixmap to analyze in detail to produce a palette
     * @param threshold the minimum difference between colors required to put them in the palette (default 100)
     */
    public FastPalette(Pixmap pixmap, double threshold) {
        super(pixmap, threshold);
    }

    /**
     * Analyzes {@code pixmap} for color count and frequency, building a palette with at most {@code limit} colors.
     * If there are {@code limit} or fewer colors, this uses the exact colors (although with at most one transparent
     * color, and no alpha for other colors); this will always reserve a palette entry for transparent (even if the
     * image has no transparency) because it uses palette index 0 in its analysis step. Because calling
     * {@link #reduce(Pixmap)} (or any of PNG8's write methods) will dither colors that aren't exact, and dithering
     * works better when the palette can choose colors that are sufficiently different, this takes a threshold value to
     * determine whether it should permit a less-common color into the palette, and if the second color is different
     * enough (as measured by {@link #differenceAnalyzing(int, int)} ) by a value of at least {@code threshold}, it is allowed in
     * the palette, otherwise it is kept out for being too similar to existing colors. The threshold is usually between
     * 50 and 500, and 100 is a good default. If the threshold is too high, then some colors that would be useful to
     * smooth out subtle color changes won't get considered, and colors may change more abruptly. This doesn't return a
     * value but instead stores the palette info in this object; a PaletteReducer can be assigned to the
     * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to {@link #reduce(Pixmap)} a
     * Pixmap.
     *
     * @param pixmap    a Pixmap to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)} or by PNG8
     * @param threshold a minimum color difference as produced by {@link #differenceAnalyzing(int, int)}; usually between 50 and 500, 100 is a good default
     * @param limit     the maximum number of colors to allow in the resulting palette; typically no more than 256
     */
    @Override
    public void analyze(Pixmap pixmap, double threshold, int limit) {
        ByteBuffer pixels = pixmap.getPixels();
        boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888);
        Arrays.fill(paletteArray, 0);
        Arrays.fill(paletteMapping, (byte) 0);
        int color;
        limit = Math.min(Math.max(limit, 2), 256);
        threshold /= Math.min(0.45, Math.pow(limit + 16, 1.45) * 0.0002);
        final int width = pixmap.getWidth(), height = pixmap.getHeight();
        IntIntMap counts = new IntIntMap(limit);
        int r, g, b;
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                r = pixels.get() & 0xF8;
                g = pixels.get() & 0xF8;
                b = pixels.get() & 0xF8;
                if (!hasAlpha || (pixels.get() & 0x80) != 0) {
                    color = r << 24 | g << 16 | b << 8;
                    color |= (color >>> 5 & 0x07070700) | 0xFF;
                    counts.getAndIncrement(color, 0, 1);
                }
            }
        }
        int cs = counts.size;
        Array es = new Array<>(cs);
        for(IntIntMap.Entry e : counts)
        {
            IntIntMap.Entry e2 = new IntIntMap.Entry();
            e2.key = e.key;
            e2.value = e.value;
            es.add(e2);
        }
        es.sort(entryComparator);
        if (cs < limit) {
            int i = 1;
            for(IntIntMap.Entry e : es) {
                color = e.key;
                paletteArray[i] = color;
                paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i;
                i++;
            }
            colorCount = i;
            populationBias = (float) Math.exp(-1.125/colorCount);
        } else // reduce color count
        {
            int i = 1, c = 0;
            PER_BEST:
            while (i < limit && c < cs) {
                color = es.get(c++).key;
                for (int j = 1; j < i; j++) {
                    if (differenceAnalyzing(color, paletteArray[j]) < threshold)
                        continue PER_BEST;
                }
                paletteArray[i] = color;
                paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i;
                i++;
            }
            colorCount = i;
            populationBias = (float) Math.exp(-1.125/colorCount);
        }
        if(reverseMap == null)
            reverseMap = new IntIntMap(colorCount);
        else
            reverseMap.clear(colorCount);

        for (int i = 0; i < colorCount; i++) {
            reverseMap.put(paletteArray[i], i);
        }
        int c2;
        int rr, gg, bb;
        double dist;
        for (r = 0; r < 32; r++) {
            rr = (r << 3 | r >>> 2);
            for (g = 0; g < 32; g++) {
                gg = (g << 3 | g >>> 2);
                for (b = 0; b < 32; b++) {
                    c2 = r << 10 | g << 5 | b;
                    if (paletteMapping[c2] == 0) {
                        bb = (b << 3 | b >>> 2);
                        dist = Double.MAX_VALUE;
                        for (int i = 1; i < colorCount; i++) {
                            if (dist > (dist = Math.min(dist, differenceAnalyzing(paletteArray[i], rr, gg, bb))))
                                paletteMapping[c2] = (byte) i;
                        }
                    }
                }
            }
        }
        pixels.rewind();
    }


    /**
     * Analyzes {@code pixmap} for color count and frequency, building a palette with at most {@code limit} colors.
     * If there are {@code limit} or fewer colors, this uses the exact colors (although with at most one transparent
     * color, and no alpha for other colors); if there are more than {@code limit} colors or any colors have 50% or less
     * alpha, it will reserve a palette entry for transparent (even if the image has no transparency). Because calling
     * {@link #reduce(Pixmap)} (or any of PNG8's write methods) will dither colors that aren't exact, and dithering
     * works better when the palette can choose colors that are sufficiently different, this takes a threshold value to
     * determine whether it should permit a less-common color into the palette, and if the second color is different
     * enough (as measured by {@link #differenceAnalyzing(int, int)} ) by a value of at least {@code threshold}, it is allowed in
     * the palette, otherwise it is kept out for being too similar to existing colors. The threshold is usually between
     * 50 and 500, and 100 is a good default. If the threshold is too high, then some colors that would be useful to
     * smooth out subtle color changes won't get considered, and colors may change more abruptly. This doesn't return a
     * value but instead stores the palette info in this object; a PaletteReducer can be assigned to the
     * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to {@link #reduce(Pixmap)} a
     * Pixmap.
     * 
* This does a faster and less accurate analysis, and is more suitable to do on each frame of a large animation when * time is better spent making more images than fewer images at higher quality. It should be about 5 times faster * than {@link #analyze(Pixmap, double, int)} with the same parameters. * * @param pixmap a Pixmap to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)} or by PNG8 * @param threshold a minimum color difference as produced by {@link #differenceAnalyzing(int, int)}; usually between 50 and 500, 100 is a good default * @param limit the maximum number of colors to allow in the resulting palette; typically no more than 256 */ @Override public void analyzeFast(Pixmap pixmap, double threshold, int limit) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); Arrays.fill(workspace, (byte) 0); int color; limit = Math.min(Math.max(limit, 2), 256); threshold /= Math.min(0.45, Math.pow(limit + 16, 1.45) * 0.0002); final int width = pixmap.getWidth(), height = pixmap.getHeight(); IntIntMap counts = new IntIntMap(limit); int r, g, b; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { r = pixels.get() & 0xF8; g = pixels.get() & 0xF8; b = pixels.get() & 0xF8; if (!hasAlpha || (pixels.get() & 0x80) != 0) { color = r << 24 | g << 16 | b << 8; color |= (color >>> 5 & 0x07070700) | 0xFF; counts.getAndIncrement(color, 0, 1); } } } int cs = counts.size; Array es = new Array<>(cs); for(IntIntMap.Entry e : counts) { IntIntMap.Entry e2 = new IntIntMap.Entry(); e2.key = e.key; e2.value = e.value; es.add(e2); } es.sort(entryComparator); if (cs < limit) { int i = 1; for(IntIntMap.Entry e : es) { color = e.key; paletteArray[i] = color; color = (color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F); workspace[color] = paletteMapping[color] = (byte) i; ++i; } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } else // reduce color count { int i = 1, c = 0; PER_BEST: while (i < limit && c < cs) { color = es.get(c++).key; for (int j = 1; j < i; j++) { if (differenceAnalyzing(color, paletteArray[j]) < threshold) continue PER_BEST; } paletteArray[i] = color; color = (color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F); workspace[color] = paletteMapping[color] = (byte) i; i++; } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < colorCount; i++) { reverseMap.put(paletteArray[i], i); } if(colorCount <= 1) return; int c2; byte bt; int numUnassigned = 1, iterations = 0; while (numUnassigned != 0) { numUnassigned = 0; for (r = 0; r < 32; r++) { for (g = 0; g < 32; g++) { for (b = 0; b < 32; b++) { c2 = r << 10 | g << 5 | b; if (workspace[c2] == 0) { if(iterations++ != 2){ if (b < 31 && (bt = paletteMapping[c2 + 1]) != 0) workspace[c2] = bt; else if (g < 31 && (bt = paletteMapping[c2 + 32]) != 0) workspace[c2] = bt; else if (r < 31 && (bt = paletteMapping[c2 + 1024]) != 0) workspace[c2] = bt; else if (b > 0 && (bt = paletteMapping[c2 - 1]) != 0) workspace[c2] = bt; else if (g > 0 && (bt = paletteMapping[c2 - 32]) != 0) workspace[c2] = bt; else if (r > 0 && (bt = paletteMapping[c2 - 1024]) != 0) workspace[c2] = bt; else numUnassigned++; } else { iterations = 0; if (b < 31 && (bt = paletteMapping[c2 + 1]) != 0) workspace[c2] = bt; else if (g < 31 && (bt = paletteMapping[c2 + 32]) != 0) workspace[c2] = bt; else if (r < 31 && (bt = paletteMapping[c2 + 1024]) != 0) workspace[c2] = bt; else if (b > 0 && (bt = paletteMapping[c2 - 1]) != 0) workspace[c2] = bt; else if (g > 0 && (bt = paletteMapping[c2 - 32]) != 0) workspace[c2] = bt; else if (r > 0 && (bt = paletteMapping[c2 - 1024]) != 0) workspace[c2] = bt; else if (b < 31 && g < 31 && (bt = paletteMapping[c2 + 1 + 32]) != 0) workspace[c2] = bt; else if (b < 31 && r < 31 && (bt = paletteMapping[c2 + 1 + 1024]) != 0) workspace[c2] = bt; else if (g < 31 && r < 31 && (bt = paletteMapping[c2 + 32 + 1024]) != 0) workspace[c2] = bt; else if (b > 0 && g > 0 && (bt = paletteMapping[c2 - 1 - 32]) != 0) workspace[c2] = bt; else if (b > 0 && r > 0 && (bt = paletteMapping[c2 - 1 - 1024]) != 0) workspace[c2] = bt; else if (g > 0 && r > 0 && (bt = paletteMapping[c2 - 32 - 1024]) != 0) workspace[c2] = bt; else if (b < 31 && g > 0 && (bt = paletteMapping[c2 + 1 - 32]) != 0) workspace[c2] = bt; else if (b < 31 && r > 0 && (bt = paletteMapping[c2 + 1 - 1024]) != 0) workspace[c2] = bt; else if (g < 31 && r > 0 && (bt = paletteMapping[c2 + 32 - 1024]) != 0) workspace[c2] = bt; else if (b > 0 && g < 31 && (bt = paletteMapping[c2 - 1 + 32]) != 0) workspace[c2] = bt; else if (b > 0 && r < 31 && (bt = paletteMapping[c2 - 1 + 1024]) != 0) workspace[c2] = bt; else if (g > 0 && r < 31 && (bt = paletteMapping[c2 - 32 + 1024]) != 0) workspace[c2] = bt; else numUnassigned++; } } } } } System.arraycopy(workspace, 0, paletteMapping, 0, 0x8000); } pixels.rewind(); } /** * Analyzes all the Pixmap items in {@code pixmaps} for color count and frequency (as if they are one image), * building a palette with at most {@code limit} colors. If there are {@code limit} or less colors, this uses the * exact colors (although with at most one transparent color, and no alpha for other colors); if there are more than * {@code limit} colors or any colors have 50% or less alpha, it will reserve a palette entry for transparent (even * if the image has no transparency). Because calling {@link #reduce(Pixmap)} (or any of PNG8's write methods) will * dither colors that aren't exact, and dithering works better when the palette can choose colors that are * sufficiently different, this takes a threshold value to determine whether it should permit a less-common color * into the palette, and if the second color is different enough (as measured by {@link #differenceAnalyzing(int, int)}) by a * value of at least {@code threshold}, it is allowed in the palette, otherwise it is kept out for being too similar * to existing colors. The threshold is usually between 50 and 500, and 100 is a good default. This doesn't return * a value but instead stores the palette info in this object; a PaletteReducer can be assigned to the * {@link PNG8#palette} or {@link AnimatedGif#palette} fields, or can be used directly to * {@link #reduce(Pixmap)} a Pixmap. * * @param pixmaps a Pixmap array to analyze, making a palette which can be used by this to {@link #reduce(Pixmap)}, by AnimatedGif, or by PNG8 * @param pixmapCount the maximum number of Pixmap entries in pixmaps to use * @param threshold a minimum color difference as produced by {@link #differenceAnalyzing(int, int)}; usually between 50 and 500, 100 is a good default * @param limit the maximum number of colors to allow in the resulting palette; typically no more than 256 */ @Override public void analyze(Pixmap[] pixmaps, int pixmapCount, double threshold, int limit) { Arrays.fill(paletteArray, 0); Arrays.fill(paletteMapping, (byte) 0); int color; limit = Math.min(Math.max(limit, 2), 256); threshold /= Math.min(0.45, Math.pow(limit + 16, 1.45) * 0.0002); IntIntMap counts = new IntIntMap(limit); int[] reds = new int[limit], greens = new int[limit], blues = new int[limit]; for (int i = 0; i < pixmapCount && i < pixmaps.length; i++) { Pixmap pixmap = pixmaps[i]; ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); final int width = pixmap.getWidth(), height = pixmap.getHeight(); int r, g, b; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { r = pixels.get() & 0xF8; g = pixels.get() & 0xF8; b = pixels.get() & 0xF8; if (!hasAlpha || (pixels.get() & 0x80) != 0) { color = r << 24 | g << 16 | b << 8; color |= (color >>> 5 & 0x07070700) | 0xFF; counts.getAndIncrement(color, 0, 1); } } } pixels.rewind(); } final int cs = counts.size; Array es = new Array<>(cs); for(IntIntMap.Entry e : counts) { IntIntMap.Entry e2 = new IntIntMap.Entry(); e2.key = e.key; e2.value = e.value; es.add(e2); } es.sort(entryComparator); if (cs < limit) { int i = 1; for(IntIntMap.Entry e : es) { color = e.key; paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; reds[i] = color >>> 24; greens[i] = color >>> 16 & 255; blues[i] = color >>> 8 & 255; i++; } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } else // reduce color count { int i = 1, c = 0; PER_BEST: for (; i < limit && c < cs;) { color = es.get(c++).key; for (int j = 1; j < i; j++) { double diff = differenceAnalyzing(color, paletteArray[j]); if (diff < threshold) continue PER_BEST; } paletteArray[i] = color; paletteMapping[(color >>> 17 & 0x7C00) | (color >>> 14 & 0x3E0) | (color >>> 11 & 0x1F)] = (byte) i; reds[i] = color >>> 24; greens[i] = color >>> 16 & 255; blues[i] = color >>> 8 & 255; i++; } colorCount = i; populationBias = (float) Math.exp(-1.125/colorCount); } if(reverseMap == null) reverseMap = new IntIntMap(colorCount); else reverseMap.clear(colorCount); for (int i = 0; i < colorCount; i++) { reverseMap.put(paletteArray[i], i); } int c2; int rr, gg, bb; double dist; for (int r = 0; r < 32; r++) { rr = (r << 3 | r >>> 2); for (int g = 0; g < 32; g++) { gg = (g << 3 | g >>> 2); for (int b = 0; b < 32; b++) { c2 = r << 10 | g << 5 | b; if (paletteMapping[c2] == 0) { bb = (b << 3 | b >>> 2); dist = Double.MAX_VALUE; for (int i = 1; i < colorCount; i++) { if (dist > (dist = Math.min(dist, differenceAnalyzing(reds[i], greens[i], blues[i], rr, gg, bb)))) paletteMapping[c2] = (byte) i; } } } } } } protected int writePixel(ByteBuffer pixels, int shrunkColor, boolean isRGBA) { int rgba = paletteArray[paletteMapping[shrunkColor] & 0xFF]; if (isRGBA) { pixels.position(pixels.position() - 4); pixels.putInt(rgba); } else { // read and put just RGB888 pixels.position(pixels.position() - 3); pixels.put((byte) (rgba >>> 24)).put((byte) (rgba >>> 16)).put((byte) (rgba >>> 8)); } return rgba; } /** * Modifies the given Pixmap so that it only uses colors present in this PaletteReducer, without dithering. This * produces blocky solid sections of color in most images where the palette isn't exact, instead of * checkerboard-like dithering patterns. If you want to reduce the colors in a Pixmap based on what it currently * contains, call {@link #analyze(Pixmap)} with {@code pixmap} as its argument, then call this method with the same * Pixmap. You may instead want to use a known palette instead of one computed from a Pixmap; * {@link #exact(int[])} is the tool for that job. * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ @Override public Pixmap reduceSolid (Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { int rr = pixels.get() & 0xF8; int gg = pixels.get() & 0xF8; int bb = pixels.get() & 0xF8; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } writePixel(pixels, (rr << 7) | (gg << 2) | (bb >>> 3), hasAlpha); } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } /** * Modifies the given Pixmap so that it only uses colors present in this PaletteReducer, dithering when it can with * Sierra Lite dithering instead of the Floyd-Steinberg dithering that {@link #reduce(Pixmap)} uses. * If you want to reduce the colors in a Pixmap based on what it currently contains, call * {@link #analyze(Pixmap)} with {@code pixmap} as its argument, then call this method with the same * Pixmap. You may instead want to use a known palette instead of one computed from a Pixmap; * {@link #exact(int[])} is the tool for that job. *

* This method is similar to Floyd-Steinberg, since both are error-diffusion dithers. Sometimes Sierra Lite can * avoid unpleasant artifacts in Floyd-Steinberg, so it's better in the worst-case, but it isn't usually as good in * its best-case. * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ public Pixmap reduceSierraLite (Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int used; float rdiff, gdiff, bdiff; float er, eg, eb; float ditherStrength = this.ditherStrength * 20, halfDitherStrength = ditherStrength * 0.5f; for (int y = 0; y < h; y++) { int ny = y + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { int rr = pixels.get() & 0xFF; int gg = pixels.get() & 0xFF; int bb = pixels.get() & 0xFF; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } er = curErrorRed[px]; eg = curErrorGreen[px]; eb = curErrorBlue[px]; int ar = Math.min(Math.max((int) (rr + er + 0.5f), 0), 0xFF); int ag = Math.min(Math.max((int) (gg + eg + 0.5f), 0), 0xFF); int ab = Math.min(Math.max((int) (bb + eb + 0.5f), 0), 0xFF); used = writePixel(pixels, ((ar << 7) & 0x7C00) | ((ag << 2) & 0x3E0) | ((ab >>> 3)), hasAlpha); rdiff = (0x2.4p-8f * (rr - (used >>> 24))); gdiff = (0x2.4p-8f * (gg - (used >>> 16 & 255))); bdiff = (0x2.4p-8f * (bb - (used >>> 8 & 255))); rdiff *= 1.25f / (0.25f + Math.abs(rdiff)); gdiff *= 1.25f / (0.25f + Math.abs(gdiff)); bdiff *= 1.25f / (0.25f + Math.abs(bdiff)); if (px < lineLen - 1) { curErrorRed[px + 1] += rdiff * ditherStrength; curErrorGreen[px + 1] += gdiff * ditherStrength; curErrorBlue[px + 1] += bdiff * ditherStrength; } if (ny < h) { if (px > 0) { nextErrorRed[px - 1] += rdiff * halfDitherStrength; nextErrorGreen[px - 1] += gdiff * halfDitherStrength; nextErrorBlue[px - 1] += bdiff * halfDitherStrength; } nextErrorRed[px] += rdiff * halfDitherStrength; nextErrorGreen[px] += gdiff * halfDitherStrength; nextErrorBlue[px] += bdiff * halfDitherStrength; } } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } /** * Modifies the given Pixmap so that it only uses colors present in this PaletteReducer, dithering when it can with * the commonly-used Floyd-Steinberg dithering. If you want to reduce the colors in a Pixmap based on what it * currently contains, call {@link #analyze(Pixmap)} with {@code pixmap} as its argument, then call this method with * the same Pixmap. You may instead want to use a known palette instead of one computed from a Pixmap; * {@link #exact(int[])} is the tool for that job. * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ public Pixmap reduceFloydSteinberg (Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int used; float rdiff, gdiff, bdiff; float er, eg, eb; float w1 = ditherStrength * 4, w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f; for (int y = 0; y < h; y++) { int ny = y + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { int rr = pixels.get() & 0xFF; int gg = pixels.get() & 0xFF; int bb = pixels.get() & 0xFF; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } er = curErrorRed[px]; eg = curErrorGreen[px]; eb = curErrorBlue[px]; int ar = Math.min(Math.max((int) (rr + er + 0.5f), 0), 0xFF); int ag = Math.min(Math.max((int) (gg + eg + 0.5f), 0), 0xFF); int ab = Math.min(Math.max((int) (bb + eb + 0.5f), 0), 0xFF); used = writePixel(pixels, ((ar << 7) & 0x7C00) | ((ag << 2) & 0x3E0) | ((ab >>> 3)), hasAlpha); rdiff = (0x1.8p-8f * (rr - (used >>> 24))); gdiff = (0x1.8p-8f * (gg - (used >>> 16 & 255))); bdiff = (0x1.8p-8f * (bb - (used >>> 8 & 255))); rdiff *= 1.25f / (0.25f + Math.abs(rdiff)); gdiff *= 1.25f / (0.25f + Math.abs(gdiff)); bdiff *= 1.25f / (0.25f + Math.abs(bdiff)); if (px < lineLen - 1) { curErrorRed[px + 1] += rdiff * w7; curErrorGreen[px + 1] += gdiff * w7; curErrorBlue[px + 1] += bdiff * w7; } if (ny < h) { if (px > 0) { nextErrorRed[px - 1] += rdiff * w3; nextErrorGreen[px - 1] += gdiff * w3; nextErrorBlue[px - 1] += bdiff * w3; } if (px < lineLen - 1) { nextErrorRed[px + 1] += rdiff * w1; nextErrorGreen[px + 1] += gdiff * w1; nextErrorBlue[px + 1] += bdiff * w1; } nextErrorRed[px] += rdiff * w5; nextErrorGreen[px] += gdiff * w5; nextErrorBlue[px] += bdiff * w5; } } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } /** * It's interleaved gradient noise, by Jorge Jimenez! It's very fast! It's an ordered dither! * It's pretty good with gradients, though it may introduce artifacts. It has noticeable diagonal * lines in some places, but these tend to have mixed directions that obscure larger patterns. * This is very similar to {@link #reduceRoberts(Pixmap)}, but has different artifacts, and this * dither tends to be stronger by default. * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceJimenez(Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); float adj; final float strength = 60f * ditherStrength / (populationBias * populationBias); for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { int rr = pixels.get() & 0xFF; int gg = pixels.get() & 0xFF; int bb = pixels.get() & 0xFF; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } adj = (px * 0.06711056f + y * 0.00583715f); adj -= (int) adj; adj *= 52.9829189f; adj -= (int) adj; adj -= 0.5f; adj *= strength; adj += 0.5f; // for rounding int ar = Math.min(Math.max((int)(rr + adj), 0), 255); int ag = Math.min(Math.max((int)(gg + adj), 0), 255); int ab = Math.min(Math.max((int)(bb + adj), 0), 255); writePixel(pixels, ((ar << 7) & 0x7C00) | ((ag << 2) & 0x3E0) | ((ab >>> 3)), hasAlpha); } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } /** * Calculates the R2 dither for an x, y point and returns a value between -1.25f and 1.25f . Because this uses the * R2 low-discrepancy sequence, adjacent x, y points almost never have similar values returned. * @param x x position, as an int; may be positive or negative * @param y y position, as an int; may be positive or negative * @return a float between -1.25f and 1.25f */ protected float roberts125(int x, int y) { return (((x * 0xC13FA9A902A6328FL + y * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-22f - 0x1.4p0f); // final float s = (((x * 0xC13FA9A902A6328FL + y * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-22f - 0x1.4p0f); // return 1.25f * s / (0.4f + Math.abs(s)); // return s * Math.abs(s); } /** * An experimental mix of an error-diffusion dither with interleaved gradient noise; like * {@link #reduceNeue(Pixmap)} mixed with {@link #reduceJimenez(Pixmap)} and without using blue noise. * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceIgneous(Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int used; float rdiff, gdiff, bdiff; float er, eg, eb; float w1 = (6f * ditherStrength * populationBias * populationBias), w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f, strength = 60f * ditherStrength / (populationBias * populationBias), adj; for (int y = 0; y < h; y++) { int ny = y + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { int rr = pixels.get() & 0xFF; int gg = pixels.get() & 0xFF; int bb = pixels.get() & 0xFF; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } { adj = (px * 0.06711056f + y * 0.00583715f); adj -= (int) adj; adj *= 52.9829189f; adj -= (int) adj; adj -= 0.5f; adj *= strength; er = adj + (curErrorRed[px]); eg = adj + (curErrorGreen[px]); eb = adj + (curErrorBlue[px]); int ar = Math.min(Math.max((int)(rr + er + 0.5f), 0), 0xFF); int ag = Math.min(Math.max((int)(gg + eg + 0.5f), 0), 0xFF); int ab = Math.min(Math.max((int)(bb + eb + 0.5f), 0), 0xFF); used = writePixel(pixels, ((ar << 7) & 0x7C00) | ((ag << 2) & 0x3E0) | ((ab >>> 3)), hasAlpha); rdiff = (0x3p-10f * (rr - (used>>>24)) ); gdiff = (0x3p-10f * (gg - (used>>>16&255))); bdiff = (0x3p-10f * (bb - (used>>>8&255)) ); if(px < lineLen - 1) { curErrorRed[px+1] += rdiff * w7; curErrorGreen[px+1] += gdiff * w7; curErrorBlue[px+1] += bdiff * w7; } if(ny < h) { if(px > 0) { nextErrorRed[px-1] += rdiff * w3; nextErrorGreen[px-1] += gdiff * w3; nextErrorBlue[px-1] += bdiff * w3; } if(px < lineLen - 1) { nextErrorRed[px+1] += rdiff * w1; nextErrorGreen[px+1] += gdiff * w1; nextErrorBlue[px+1] += bdiff * w1; } nextErrorRed[px] += rdiff * w5; nextErrorGreen[px] += gdiff * w5; nextErrorBlue[px] += bdiff * w5; } } } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } /** * An ordered dither that uses a sub-random sequence by Martin Roberts to disperse lightness adjustments across the * image. This is very similar to {@link #reduceJimenez(Pixmap)}, but is milder by default, and has subtly different * artifacts. This should look excellent for animations, especially with small palettes, but the lightness * adjustments may be noticeable even in very large palettes. * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceRoberts (Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); // float str = (32f * ditherStrength / (populationBias * populationBias)); // float str = (float) (64 * ditherStrength / Math.log(colorCount * 0.3 + 1.5)); float str = (25f * ditherStrength / (populationBias * populationBias * populationBias * populationBias)); for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { int rr = pixels.get() & 0xFF; int gg = pixels.get() & 0xFF; int bb = pixels.get() & 0xFF; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } // used in 0.3.10 // // Gets R2-based noise and puts it in the -0.75 to 0.75 range // float adj = (px * 0xC13FA9A902A6328FL + y * 0x91E10DA5C79E7B1DL >>> 41) * 0x1.8p-23f - 0.75f; // adj = adj * str + 0.5f; // int rr = Math.min(Math.max((int)(((color >>> 24) ) + adj), 0), 255); // int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + adj), 0), 255); // int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + adj), 0), 255); // other options // // sign-preserving square root, emphasizes extremes //// adj = Math.copySign((float) Math.sqrt(Math.abs(adj)), adj); // // sign-preserving square, emphasizes low-magnitude values //// adj *= Math.abs(adj); int ar = Math.min(Math.max((int) (rr + roberts125(px - 1, y + 1) * str + 0.5f), 0), 255); int ag = Math.min(Math.max((int) (gg + roberts125(px + 3, y - 1) * str + 0.5f), 0), 255); int ab = Math.min(Math.max((int) (bb + roberts125(px - 4, y + 2) * str + 0.5f), 0), 255); // int ar = Math.min(Math.max((int) (rr + ((((px - 1) * 0xC13FA9A902A6328FL + (y + 1) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-22f - 0x1.4p0f) * str + 0.5f), 0), 255); // int ag = Math.min(Math.max((int) (gg + ((((px + 3) * 0xC13FA9A902A6328FL + (y - 1) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-22f - 0x1.4p0f) * str + 0.5f), 0), 255); // int ab = Math.min(Math.max((int) (bb + ((((px + 2) * 0xC13FA9A902A6328FL + (y + 3) * 0x91E10DA5C79E7B1DL) >>> 41) * 0x1.4p-22f - 0x1.4p0f) * str + 0.5f), 0), 255); writePixel(pixels, ((ar << 7) & 0x7C00) | ((ag << 2) & 0x3E0) | ((ab >>> 3)), hasAlpha); } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } public Pixmap reduceWoven(Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int used; float rdiff, gdiff, bdiff; float er, eg, eb; float w1 = (float) (20f * Math.sqrt(ditherStrength) * populationBias * populationBias * populationBias * populationBias), w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f, strength = 24f * ditherStrength / (populationBias * populationBias * populationBias * populationBias), limit = 5f + 110f / (float) Math.sqrt(colorCount + 1.5f); for (int y = 0; y < h; y++) { int ny = y + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { int rr = pixels.get() & 0xFF; int gg = pixels.get() & 0xFF; int bb = pixels.get() & 0xFF; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } er = Math.min(Math.max(roberts125(px - 1, y + 1) * strength, -limit), limit) + (curErrorRed[px]); eg = Math.min(Math.max(roberts125(px + 3, y - 1) * strength, -limit), limit) + (curErrorGreen[px]); eb = Math.min(Math.max(roberts125(px - 4, y + 2) * strength, -limit), limit) + (curErrorBlue[px]); int ar = Math.min(Math.max((int) (rr + er + 0.5f), 0), 0xFF); int ag = Math.min(Math.max((int) (gg + eg + 0.5f), 0), 0xFF); int ab = Math.min(Math.max((int) (bb + eb + 0.5f), 0), 0xFF); used = writePixel(pixels, ((ar << 7) & 0x7C00) | ((ag << 2) & 0x3E0) | ((ab >>> 3)), hasAlpha); rdiff = (0x5p-10f * (rr - (used >>> 24))); gdiff = (0x5p-10f * (gg - (used >>> 16 & 255))); bdiff = (0x5p-10f * (bb - (used >>> 8 & 255))); if (px < lineLen - 1) { curErrorRed[px + 1] += rdiff * w7; curErrorGreen[px + 1] += gdiff * w7; curErrorBlue[px + 1] += bdiff * w7; } if (ny < h) { if (px > 0) { nextErrorRed[px - 1] += rdiff * w3; nextErrorGreen[px - 1] += gdiff * w3; nextErrorBlue[px - 1] += bdiff * w3; } if (px < lineLen - 1) { nextErrorRed[px + 1] += rdiff * w1; nextErrorGreen[px + 1] += gdiff * w1; nextErrorBlue[px + 1] += bdiff * w1; } nextErrorRed[px] += rdiff * w5; nextErrorGreen[px] += gdiff * w5; nextErrorBlue[px] += bdiff * w5; } } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } /** * A blue-noise-based dither; does not diffuse error, and uses a tiling blue noise pattern (which can be accessed * with {@link #TRI_BLUE_NOISE}, but shouldn't usually be modified) as well as a 8x8 threshold matrix (the kind * used by {@link #reduceKnoll(Pixmap)}, but larger). This has a tendency to look closer to a color * reduction with no dither (as with {@link #reduceSolid(Pixmap)} than to one with too much dither. Because it is an * ordered dither, it avoids "swimming" patterns in animations with large flat sections of one color; these swimming * effects can appear in all the error-diffusion dithers here. If you can tolerate "spongy" artifacts appearing * (which look worse on small palettes), you may get very good handling of lightness by raising dither strength. * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceBlueNoise (Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); float adj, strength = 32f * ditherStrength / (populationBias); for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { int rr = pixels.get() & 0xFF; int gg = pixels.get() & 0xFF; int bb = pixels.get() & 0xFF; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } // float pos = (PaletteReducer.thresholdMatrix64[(px & 7) | (y & 7) << 3] - 31.5f) * 0.2f + 0.5f; // The line below is a sigmoid function; it ranges from -strength to strength, depending on adj. // Using 12f makes the slope more shallow, where a smaller number would make it steep near adj == 0. //adj = adj * strength / (12f + Math.abs(adj)); adj = ((PaletteReducer.TRI_BLUE_NOISE_B[(px & 63) | (y & 63) << 6] + 0.5f)); adj = adj * strength / (12f + Math.abs(adj)); int ar = Math.min(Math.max((int) (adj + rr + 0.5f), 0), 255); adj = ((PaletteReducer.TRI_BLUE_NOISE_C[(px & 63) | (y & 63) << 6] + 0.5f)); adj = adj * strength / (12f + Math.abs(adj)); int ag = Math.min(Math.max((int) (adj + gg + 0.5f), 0), 255); adj = ((PaletteReducer.TRI_BLUE_NOISE[(px & 63) | (y & 63) << 6] + 0.5f)); adj = adj * strength / (12f + Math.abs(adj)); int ab = Math.min(Math.max((int) (adj + bb + 0.5f), 0), 255); writePixel(pixels, ((ar << 7) & 0x7C00) | ((ag << 2) & 0x3E0) | ((ab >>> 3)), hasAlpha); } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } /** * A white-noise-based dither; uses the colors encountered so far during dithering as a sort of state for basic * pseudo-random number generation, while also using some blue noise from a tiling texture to offset clumping. * This tends to be very rough-looking, and generally only looks good with larger palettes or with animations. It * could be a good aesthetic choice if you want a scratchy, "distressed-looking" image. * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceChaoticNoise (Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int used; double adj, strength = ditherStrength * populationBias * 1.5; long s = 0xC13FA9A902A6328FL; for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { int rr = pixels.get() & 0xFF; int gg = pixels.get() & 0xFF; int bb = pixels.get() & 0xFF; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } used = paletteArray[paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF]; adj = ((PaletteReducer.TRI_BLUE_NOISE[(px & 63) | (y & 63) << 6] + 0.5f) * 0.007843138f); adj *= adj * adj; //// Complicated... This starts with a checkerboard of -0.5 and 0.5, times a tiny fraction. //// The next 3 lines generate 3 low-quality-random numbers based on s, which should be //// different as long as the colors encountered so far were different. The numbers can //// each be positive or negative, and are reduced to a manageable size, summed, and //// multiplied by the earlier tiny fraction. Summing 3 random values gives us a curved //// distribution, centered on about 0.0 and weighted so most results are close to 0. //// Two of the random numbers use an XLCG, and the last uses an LCG. adj += ((px + y & 1) - 0.5f) * 0x1.8p-49 * strength * (((s ^ 0x9E3779B97F4A7C15L) * 0xC6BC279692B5CC83L >> 15) + ((~s ^ 0xDB4F0B9175AE2165L) * 0xD1B54A32D192ED03L >> 15) + ((s = (s ^ rr + gg + bb) * 0xD1342543DE82EF95L + 0x91E10DA5C79E7B1DL) >> 15)); int ar = Math.min(Math.max((int) (rr + (adj * ((rr - (used >>> 24))))), 0), 0xFF); int ag = Math.min(Math.max((int) (gg + (adj * ((gg - (used >>> 16 & 0xFF))))), 0), 0xFF); int ab = Math.min(Math.max((int) (bb + (adj * ((bb - (used >>> 8 & 0xFF))))), 0), 0xFF); writePixel(pixels, ((ar << 7) & 0x7C00) | ((ag << 2) & 0x3E0) | ((ab >>> 3)), hasAlpha); } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } /** * Modifies the given Pixmap so it only uses colors present in this PaletteReducer, using Floyd-Steinberg to dither * but modifying patterns slightly by introducing triangular-distributed blue noise. If you want to reduce the * colors in a Pixmap based on what it currently contains, call {@link #analyze(Pixmap)} with {@code pixmap} as its * argument, then call this method with the same Pixmap. You may instead want to use a known palette instead of one * computed from a Pixmap; {@link #exact(int[])} is the tool for that job. * @param pixmap a Pixmap that will be modified in place * @return the given Pixmap, for chaining */ public Pixmap reduceScatter (Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int used; float rdiff, gdiff, bdiff; float er, eg, eb; float w1 = ditherStrength * 3.5f, w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f; for (int y = 0; y < h; y++) { int ny = y + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { int rr = pixels.get() & 0xFF; int gg = pixels.get() & 0xFF; int bb = pixels.get() & 0xFF; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } float tbn = PaletteReducer.TRI_BLUE_NOISE_MULTIPLIERS[(px & 63) | ((y << 6) & 0xFC0)]; er = curErrorRed[px] * tbn; eg = curErrorGreen[px] * tbn; eb = curErrorBlue[px] * tbn; int ar = Math.min(Math.max((int) (rr + er + 0.5f), 0), 0xFF); int ag = Math.min(Math.max((int) (gg + eg + 0.5f), 0), 0xFF); int ab = Math.min(Math.max((int) (bb + eb + 0.5f), 0), 0xFF); used = writePixel(pixels, ((ar << 7) & 0x7C00) | ((ag << 2) & 0x3E0) | ((ab >>> 3)), hasAlpha); rdiff = (0x2.Ep-8f * (rr - (used >>> 24))); gdiff = (0x2.Ep-8f * (gg - (used >>> 16 & 255))); bdiff = (0x2.Ep-8f * (bb - (used >>> 8 & 255))); rdiff *= 1.25f / (0.25f + Math.abs(rdiff)); gdiff *= 1.25f / (0.25f + Math.abs(gdiff)); bdiff *= 1.25f / (0.25f + Math.abs(bdiff)); if (px < lineLen - 1) { curErrorRed[px + 1] += rdiff * w7; curErrorGreen[px + 1] += gdiff * w7; curErrorBlue[px + 1] += bdiff * w7; } if (ny < h) { if (px > 0) { nextErrorRed[px - 1] += rdiff * w3; nextErrorGreen[px - 1] += gdiff * w3; nextErrorBlue[px - 1] += bdiff * w3; } if (px < lineLen - 1) { nextErrorRed[px + 1] += rdiff * w1; nextErrorGreen[px + 1] += gdiff * w1; nextErrorBlue[px + 1] += bdiff * w1; } nextErrorRed[px] += rdiff * w5; nextErrorGreen[px] += gdiff * w5; nextErrorBlue[px] += bdiff * w5; } } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } /** * An error-diffusion dither based on {@link #reduceFloydSteinberg(Pixmap)}, but adding in triangular-mapped blue * noise before diffusing, like {@link #reduceBlueNoise(Pixmap)}. This looks like {@link #reduceScatter(Pixmap)} in * many cases, but smooth gradients are much smoother with Neue than Scatter. Scatter multiplies error by a blue * noise value, where this adds blue noise regardless of error. This also preserves color better than TrueBlue, * while keeping similar gradient smoothness. The algorithm here uses a 2x2 rough checkerboard pattern to offset * some roughness that can appear in blue noise; the checkerboard can appear in some cases when a dithered image is * zoomed with certain image filters. *
* Neue is a German word for "new," and this is a new look at Scatter's technique. * @param pixmap will be modified in-place and returned * @return pixmap, after modifications */ public Pixmap reduceNeue(Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue; if (curErrorRedFloats == null) { curErrorRed = (curErrorRedFloats = new FloatArray(lineLen)).items; nextErrorRed = (nextErrorRedFloats = new FloatArray(lineLen)).items; curErrorGreen = (curErrorGreenFloats = new FloatArray(lineLen)).items; nextErrorGreen = (nextErrorGreenFloats = new FloatArray(lineLen)).items; curErrorBlue = (curErrorBlueFloats = new FloatArray(lineLen)).items; nextErrorBlue = (nextErrorBlueFloats = new FloatArray(lineLen)).items; } else { curErrorRed = curErrorRedFloats.ensureCapacity(lineLen); nextErrorRed = nextErrorRedFloats.ensureCapacity(lineLen); curErrorGreen = curErrorGreenFloats.ensureCapacity(lineLen); nextErrorGreen = nextErrorGreenFloats.ensureCapacity(lineLen); curErrorBlue = curErrorBlueFloats.ensureCapacity(lineLen); nextErrorBlue = nextErrorBlueFloats.ensureCapacity(lineLen); for (int i = 0; i < lineLen; i++) { nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } } Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int used; float rdiff, gdiff, bdiff; float er, eg, eb; float w1 = ditherStrength * 7f, w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f, adj, strength = (32f * ditherStrength / (populationBias * populationBias * populationBias)), limit = (float) Math.pow(80, 1.635 - populationBias); for (int py = 0; py < h; py++) { int ny = py + 1; for (int i = 0; i < lineLen; i++) { curErrorRed[i] = nextErrorRed[i]; curErrorGreen[i] = nextErrorGreen[i]; curErrorBlue[i] = nextErrorBlue[i]; nextErrorRed[i] = 0; nextErrorGreen[i] = 0; nextErrorBlue[i] = 0; } for (int px = 0; px < lineLen; px++) { int rr = pixels.get() & 0xFF; int gg = pixels.get() & 0xFF; int bb = pixels.get() & 0xFF; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } adj = ((TRI_BLUE_NOISE[(px & 63) | (py & 63) << 6] + 0.5f) * 0.005f); // plus or minus 255/400 adj = Math.min(Math.max(adj * strength, -limit), limit) + 0.5f; er = adj + (curErrorRed[px]); eg = adj + (curErrorGreen[px]); eb = adj + (curErrorBlue[px]); int ar = Math.min(Math.max((int)(rr + er), 0), 255); int ag = Math.min(Math.max((int)(gg + eg), 0), 255); int ab = Math.min(Math.max((int)(bb + eb), 0), 255); used = writePixel(pixels, ((ar << 7) & 0x7C00) | ((ag << 2) & 0x3E0) | ((ab >>> 3)), hasAlpha); rdiff = (0x1.7p-10f * (rr - (used>>>24)) ); gdiff = (0x1.7p-10f * (gg - (used>>>16&255))); bdiff = (0x1.7p-10f * (bb - (used>>>8&255)) ); rdiff *= 1.25f / (0.25f + Math.abs(rdiff)); gdiff *= 1.25f / (0.25f + Math.abs(gdiff)); bdiff *= 1.25f / (0.25f + Math.abs(bdiff)); if(px < lineLen - 1) { curErrorRed[px+1] += rdiff * w7; curErrorGreen[px+1] += gdiff * w7; curErrorBlue[px+1] += bdiff * w7; } if(ny < h) { if(px > 0) { nextErrorRed[px-1] += rdiff * w3; nextErrorGreen[px-1] += gdiff * w3; nextErrorBlue[px-1] += bdiff * w3; } if(px < lineLen - 1) { nextErrorRed[px+1] += rdiff * w1; nextErrorGreen[px+1] += gdiff * w1; nextErrorBlue[px+1] += bdiff * w1; } nextErrorRed[px] += rdiff * w5; nextErrorGreen[px] += gdiff * w5; nextErrorBlue[px] += bdiff * w5; } } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } /** * Reduces a Pixmap to the palette this knows by using Thomas Knoll's pattern dither, which is out-of-patent since * late 2019. The output this produces is very dependent on the palette and this PaletteReducer's dither strength, * which can be set with {@link #setDitherStrength(float)}. At close-up zooms, a strong grid pattern will be visible * on most dithered output (like needlepoint). The algorithm was described in detail by Joel Yliluoma in * this dithering article. Yliluoma used an 8x8 * threshold matrix because at the time 4x4 was still covered by the patent, but using 4x4 allows a much faster * sorting step (this uses a sorting network, which works well for small input sizes like 16 items). This is still * very significantly slower than the other dithers here (although {@link #reduceKnollRoberts(Pixmap)} isn't at all * fast, it still takes less than half the time this method does). *
* Using pattern dither tends to produce some of the best results for lightness-based gradients, but when viewed * close-up the "needlepoint" pattern can be jarring for images that should look natural. * @see #reduceKnollRoberts(Pixmap) An alternative that uses a similar pattern but skews it to obscure the grid * @param pixmap a Pixmap that will be modified * @return {@code pixmap}, after modifications */ public Pixmap reduceKnoll (Pixmap pixmap) { ByteBuffer pixels = pixmap.getPixels(); boolean hasAlpha = pixmap.getFormat().equals(Pixmap.Format.RGBA8888); boolean hasTransparent = (paletteArray[0] == 0); final int lineLen = pixmap.getWidth(), h = pixmap.getHeight(); Pixmap.Blending blending = pixmap.getBlending(); pixmap.setBlending(Pixmap.Blending.None); int used, usedIndex; float cr, cg, cb; final float errorMul = (ditherStrength * populationBias); for (int y = 0; y < h; y++) { for (int px = 0; px < lineLen; px++) { cr = (pixels.get() & 0xFF) + 0.5f; cg = (pixels.get() & 0xFF) + 0.5f; cb = (pixels.get() & 0xFF) + 0.5f; // read one more byte if this is RGBA8888 if (hasAlpha && hasTransparent && (pixels.get() & 0x80) == 0) { pixels.position(pixels.position() - 4); pixels.putInt(0); continue; } int er = 0, eg = 0, eb = 0; for (int i = 0; i < 16; i++) { int rr = Math.min(Math.max((int) (cr + er * errorMul), 0), 255); int gg = Math.min(Math.max((int) (cg + eg * errorMul), 0), 255); int bb = Math.min(Math.max((int) (cb + eb * errorMul), 0), 255); usedIndex = paletteMapping[((rr << 7) & 0x7C00) | ((gg << 2) & 0x3E0) | ((bb >>> 3))] & 0xFF; candidates[i | 16] = shrink(candidates[i] = used = paletteArray[usedIndex]); er += cr - (used >>> 24); eg += cg - (used >>> 16 & 0xFF); eb += cb - (used >>> 8 & 0xFF); } sort16(candidates); used = candidates[thresholdMatrix16[((px & 3) | (y & 3) << 2)]]; if (hasAlpha) { pixels.position(pixels.position() - 4); pixels.putInt(used); } else { // read and put just RGB888 pixels.position(pixels.position() - 3); pixels.put((byte) (used >>> 24)).put((byte) (used >>> 16)).put((byte) (used >>> 8)); } } } pixmap.setBlending(blending); pixels.rewind(); return pixmap; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy