Цветное квантование с N из M предопределенных цветов

У меня есть немного странная проблема, пытающаяся квантовать и сглаживать изображение RGB. В идеале я должен иметь возможность реализовать подходящий алгоритм в Java или использовать библиотеку Java, но ссылки на реализации на других языках также могут быть полезны.

В качестве входных данных указано следующее:

  • image: 24-битное растровое изображение RGB
  • palette: список цветов, определенных с их значениями RGB
  • max_cols: максимальное количество цветов, которые будут использоваться в выходном изображении

Возможно, важно, что и размер палитры, и максимальное количество разрешенных цветов не обязательно имеют мощность 2 и могут быть больше 255.

Итак, цель состоит в том, чтобы взять image, выбрать до max_cols цвета из предоставленного palette и выводить изображение, используя только выбранные цвета и отображаемые с использованием какого-то диффузного сглаживания ошибок. Какой алгоритм сглаживания не так важен, но он должен быть вариантом распространения ошибок (например, Floyd-Steinberg), а не простым полутоновым или упорядоченным сглаживанием.

Производительность не особенно важна, а размер ожидаемого ввода данных относительно невелик. Изображения редко будут размером более 500x500 пикселей, при этом палитра может содержать около 3-400 цветов, а количество цветов обычно будет меньше 100. Также можно с уверенностью предположить, что палитра содержит широкий набор цветов, охватывая изменения как оттенка, насыщенности и яркости.

Выбор палитры и сглаживание, используемые scolorq, были бы идеальными, но, похоже, нелегко адаптировать алгоритм для выбора цветов из уже определенной палитры произвольных цветов.

Чтобы быть более точным, проблема, в которой я застреваю, - это выбор подходящих цветов из предоставленной палитры. Предположим, что я, например. используйте scolorq для создания палитры с N цветами, а затем замените цвета, определенные scolorq, ближайшими цветами из предоставленной палитры, а затем используйте эти цвета в сочетании с диффузным диффузированием. Это даст результат, по крайней мере, похожий на входное изображение, но из-за непредсказуемых оттенков выбранных цветов выходное изображение может получить сильную, нежелательную окраску. Например. при использовании входного изображения с серой шкалой и палитры с несколькими нейтральными серыми тонами, но с большим количеством коричневых тонов (или, в более общем плане, с несколькими цветами с одинаковым оттенком, низкой насыщенностью и большим изменением яркости), мой цвет алгоритм выбора, похоже, предпочитает эти цвета выше нейтральных серо, поскольку коричневые тона, по крайней мере, математически ближе к желаемому цвету, чем серые. Такая же проблема сохраняется даже при преобразовании значений RGB в HSB и использовании разных весов для каналов H, S и B при поиске ближайшего доступного цвета.

Любые предложения по правильному внедрению или даже лучшая библиотека, которую я могу использовать для выполнения задачи?

Поскольку Xabster спросил, я также могу объяснить цель этим упражнением, хотя это не имеет никакого отношения к тому, как можно решить реальную проблему. Целью для выходного изображения является вышивка или рисунок гобелена. В самом простом случае каждый пиксель на выходном изображении соответствует стежке, выполненному на какой-либо несущей ткани. Палитра соответствует доступной нити, которая обычно имеет несколько сотен цветов. По практическим соображениям, однако, необходимо ограничить количество цветов, используемых в реальной работе. Гуглинг для вышивок гобелена даст несколько примеров.

И чтобы выяснить, где именно проблема... Решение действительно может быть разделено на два отдельных этапа:

  • выбор оптимального подмножества исходной палитры
  • с помощью подмножества для вывода выходного изображения

Здесь первым шагом является фактическая проблема. Если выбор палитры работает правильно, я мог бы просто использовать выбранные цвета и, например, Floyd-Steinberg сглаживает, чтобы получить разумный результат (что довольно сложно реализовать).

Если я правильно понимаю реализацию scolorq, scolorq, однако, объединяет эти два шага, используя знание алгоритма сглаживания в выборе палитры, чтобы создать еще лучший результат. Это было бы, конечно, предпочтительным решением, но алгоритмы, используемые в scolorq, немного превосходят мои математические знания.

Ответы

Ответ 1

Обзор

Это возможный подход к проблеме:

1) Каждый цвет от входных пикселей отображается на ближайший цвет из входной цветовой палитры.

2) Если результирующая палитра больше допустимого максимального количества цветов, палитра будет уменьшена до максимально допустимого числа, удалив цвета, наиболее похожие друг на друга из вычисленной палитры (я выбрал ближайшего расстояния для удаления, поэтому полученное изображение остается высоким в контрасте).

3) Если результирующая палитра меньше разрешенного максимального количества цветов, она заполняется самыми близкими цветами из оставшихся цветов входной палитры, пока не будет достигнуто допустимое количество цветов. Это делается в надежде, что алгоритм сглаживания может использовать эти цвета во время сглаживания. Обратите внимание, что я не видел большой разницы между заполнением или не заполнением палитры для алгоритма Флойда-Штайнберга...

4) В качестве последнего шага входные пиксели затухают с вычисленной палитрой.


РЕАЛИЗАЦИЯ

Ниже приведена реализация этого подхода.

Если вы хотите запустить исходный код, вам понадобится этот класс: ImageFrame.java. Вы можете установить входное изображение в качестве единственного параметра программы, все остальные параметры должны быть установлены в основном методе. Используемый алгоритм Флойда-Штайнберга - сглаживание Floyd-Steinberg.

Можно выбрать между тремя различными стратегиями сокращения для алгоритма сокращения палитры:

1) ORIGINAL_COLORS: этот алгоритм пытается как можно лучше соответствовать цветам входных пикселей, ища два цвета в палитре, которые имеют наименьшее расстояние. Из этих двух цветов он удаляет один с наименьшим отображением в пикселях на входной карте.

2) BETTER_CONTRAST: работает как ORIGINAL_COLORS, с той разницей, что из двух цветов он удаляет ту, которая имеет самое низкое среднее расстояние до остальной части палитры.

3) AVERAGE_DISTANCE: этот алгоритм всегда удаляет цвета с самым низким средним расстоянием от пула. Этот параметр может особенно улучшить качество получаемого изображения для палитр оттенков серого.

Вот полный код:

import java.awt.Color;
import java.awt.Image;
import java.awt.image.PixelGrabber;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

public class Quantize {

public static class RGBTriple {
    public final int[] channels;
    public RGBTriple() { channels = new int[3]; }

    public RGBTriple(int color) { 
        int r = (color >> 16) & 0xFF;
        int g = (color >> 8) & 0xFF;
        int b = (color >> 0) & 0xFF;
        channels = new int[]{(int)r, (int)g, (int)b};
    }
    public RGBTriple(int R, int G, int B)
    { channels = new int[]{(int)R, (int)G, (int)B}; }
}

/* The authors of this work have released all rights to it and placed it
in the public domain under the Creative Commons CC0 1.0 waiver
(http://creativecommons.org/publicdomain/zero/1.0/).

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Retrieved from: http://en.literateprograms.org/Floyd-Steinberg_dithering_(Java)?oldid=12476
 */
public static class FloydSteinbergDither
{
    private static int plus_truncate_uchar(int a, int b) {
        if ((a & 0xff) + b < 0)
            return 0;
        else if ((a & 0xff) + b > 255)
            return (int)255;
        else
            return (int)(a + b);
    }


    private static int findNearestColor(RGBTriple color, RGBTriple[] palette) {
        int minDistanceSquared = 255*255 + 255*255 + 255*255 + 1;
        int bestIndex = 0;
        for (int i = 0; i < palette.length; i++) {
            int Rdiff = (color.channels[0] & 0xff) - (palette[i].channels[0] & 0xff);
            int Gdiff = (color.channels[1] & 0xff) - (palette[i].channels[1] & 0xff);
            int Bdiff = (color.channels[2] & 0xff) - (palette[i].channels[2] & 0xff);
            int distanceSquared = Rdiff*Rdiff + Gdiff*Gdiff + Bdiff*Bdiff;
            if (distanceSquared < minDistanceSquared) {
                minDistanceSquared = distanceSquared;
                bestIndex = i;
            }
        }
        return bestIndex;
    }

    public static int[][] floydSteinbergDither(RGBTriple[][] image, RGBTriple[] palette)
    {
        int[][] result = new int[image.length][image[0].length];

        for (int y = 0; y < image.length; y++) {
            for (int x = 0; x < image[y].length; x++) {
                RGBTriple currentPixel = image[y][x];
                int index = findNearestColor(currentPixel, palette);
                result[y][x] = index;

                for (int i = 0; i < 3; i++)
                {
                    int error = (currentPixel.channels[i] & 0xff) - (palette[index].channels[i] & 0xff);
                    if (x + 1 < image[0].length) {
                        image[y+0][x+1].channels[i] =
                                plus_truncate_uchar(image[y+0][x+1].channels[i], (error*7) >> 4);
                    }
                    if (y + 1 < image.length) {
                        if (x - 1 > 0) {
                            image[y+1][x-1].channels[i] =
                                    plus_truncate_uchar(image[y+1][x-1].channels[i], (error*3) >> 4);
                        }
                        image[y+1][x+0].channels[i] =
                                plus_truncate_uchar(image[y+1][x+0].channels[i], (error*5) >> 4);
                        if (x + 1 < image[0].length) {
                            image[y+1][x+1].channels[i] =
                                    plus_truncate_uchar(image[y+1][x+1].channels[i], (error*1) >> 4);
                        }
                    }
                }
            }
        }
        return result;
    }

    public static void generateDither(int[] pixels, int[] p, int w, int h){
        RGBTriple[] palette = new RGBTriple[p.length];
        for (int i = 0; i < palette.length; i++) {
            int color = p[i];
            palette[i] = new RGBTriple(color);
        }
        RGBTriple[][] image = new RGBTriple[w][h];
        for (int x = w; x-- > 0; ) {
            for (int y = h; y-- > 0; ) {
                int index = y * w + x;
                int color = pixels[index];
                image[x][y] = new RGBTriple(color);
            }
        }

        int[][] result = floydSteinbergDither(image, palette);
        convert(result, pixels, p, w, h);

    }

    public static void convert(int[][] result, int[] pixels, int[] p, int w, int h){
        for (int x = w; x-- > 0; ) {
            for (int y = h; y-- > 0; ) {
                int index = y * w + x;
                int index2 = result[x][y];
                pixels[index] = p[index2];
            }
        }
    }
}

private static class PaletteColor{
    final int color;
    public PaletteColor(int color) {
        super();
        this.color = color;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + color;
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        PaletteColor other = (PaletteColor) obj;
        if (color != other.color)
            return false;
        return true;
    }

    public List<Integer> indices = new ArrayList<>();
}


public static int[] getPixels(Image image) throws IOException {
    int w = image.getWidth(null);
    int h = image.getHeight(null);        
    int pix[] = new int[w * h];
    PixelGrabber grabber = new PixelGrabber(image, 0, 0, w, h, pix, 0, w);

    try {
        if (grabber.grabPixels() != true) {
            throw new IOException("Grabber returned false: " +
                    grabber.status());
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return pix;
}

/**
 * Returns the color distance between color1 and color2
 */
public static float getPixelDistance(PaletteColor color1, PaletteColor color2){
    int c1 = color1.color;
    int r1 = (c1 >> 16) & 0xFF;
    int g1 = (c1 >> 8) & 0xFF;
    int b1 = (c1 >> 0) & 0xFF;
    int c2 = color2.color;
    int r2 = (c2 >> 16) & 0xFF;
    int g2 = (c2 >> 8) & 0xFF;
    int b2 = (c2 >> 0) & 0xFF;
    return (float) getPixelDistance(r1, g1, b1, r2, g2, b2);
}

public static double getPixelDistance(int r1, int g1, int b1, int r2, int g2, int b2){
    return Math.sqrt(Math.pow(r2 - r1, 2) + Math.pow(g2 - g1, 2) + Math.pow(b2 - b1, 2));
}

/**
 * Fills the given fillColors palette with the nearest colors from the given colors palette until
 * it has the given max_cols size.
 */
public static void fillPalette(List<PaletteColor> fillColors, List<PaletteColor> colors, int max_cols){
    while (fillColors.size() < max_cols) {
        int index = -1;
        float minDistance = -1;
        for (int i = 0; i < fillColors.size(); i++) {
            PaletteColor color1 = colors.get(i);
            for (int j = 0; j < colors.size(); j++) {
                PaletteColor color2 = colors.get(j);
                if (color1 == color2) {
                    continue;
                }
                float distance = getPixelDistance(color1, color2);
                if (index == -1 || distance < minDistance) {
                    index = j;
                    minDistance = distance;
                }
            }
        }
        PaletteColor color = colors.get(index);
        fillColors.add(color);
    }
}

public static void reducePaletteByAverageDistance(List<PaletteColor> colors, int max_cols, ReductionStrategy reductionStrategy){
    while (colors.size() > max_cols) {
        int index = -1;
        float minDistance = -1;
        for (int i = 0; i < colors.size(); i++) {
            PaletteColor color1 = colors.get(i);
            float averageDistance = 0;
            int count = 0;
            for (int j = 0; j < colors.size(); j++) {
                PaletteColor color2 = colors.get(j);
                if (color1 == color2) {
                    continue;
                }
                averageDistance += getPixelDistance(color1, color2);
                count++;
            }
            averageDistance/=count;
            if (minDistance == -1 || averageDistance < minDistance) {
                minDistance = averageDistance;
                index = i;
            }
        }
        PaletteColor removed = colors.remove(index);
        // find the color with the least distance:
        PaletteColor best = null;
        minDistance = -1;
        for (int i = 0; i < colors.size(); i++) {
            PaletteColor c = colors.get(i);
            float distance = getPixelDistance(c, removed);
            if (best == null || distance < minDistance) {
                best = c;
                minDistance = distance;
            }
        }
        best.indices.addAll(removed.indices);

    }
}
/**
 * Reduces the given color palette until it has the given max_cols size.
 * The colors that are closest in distance to other colors in the palette
 * get removed first.
 */
public static void reducePalette(List<PaletteColor> colors, int max_cols, ReductionStrategy reductionStrategy){
    if (reductionStrategy == ReductionStrategy.AVERAGE_DISTANCE) {
        reducePaletteByAverageDistance(colors, max_cols, reductionStrategy);
        return;
    }
    while (colors.size() > max_cols) {
        int index1 = -1;
        int index2 = -1;
        float minDistance = -1;
        for (int i = 0; i < colors.size(); i++) {
            PaletteColor color1 = colors.get(i);
            for (int j = i+1; j < colors.size(); j++) {
                PaletteColor color2 = colors.get(j);
                if (color1 == color2) {
                    continue;
                }
                float distance = getPixelDistance(color1, color2);
                if (index1 == -1 || distance < minDistance) {
                    index1 = i;
                    index2 = j;
                    minDistance = distance;
                }
            }
        }
        PaletteColor color1 = colors.get(index1);
        PaletteColor color2 = colors.get(index2);

        switch (reductionStrategy) {
            case BETTER_CONTRAST:
                // remove the color with the lower average distance to the other palette colors
                int count = 0;
                float distance1 = 0;
                float distance2 = 0;
                for (PaletteColor c : colors) {
                    if (c != color1 && c != color2) {
                        count++;
                        distance1 += getPixelDistance(color1, c);
                        distance2 += getPixelDistance(color2, c);
                    }
                }
                if (count != 0 && distance1 != distance2) {
                    distance1 /= (float)count;
                    distance2 /= (float)count;
                    if (distance1 < distance2) {
                        // remove color 1;
                        colors.remove(index1);
                        color2.indices.addAll(color1.indices);
                    } else{
                        // remove color 2;
                        colors.remove(index2);
                        color1.indices.addAll(color2.indices);
                    }
                    break;
                }
                //$FALL-THROUGH$
            default:
                // remove the color with viewer mappings to the input pixels
                if (color1.indices.size() < color2.indices.size()) {
                    // remove color 1;
                    colors.remove(index1);
                    color2.indices.addAll(color1.indices);
                } else{
                    // remove color 2;
                    colors.remove(index2);
                    color1.indices.addAll(color2.indices);
                }
                break;
        }

    }
}

/**
 * Creates an initial color palette from the given pixels and the given palette by
 * selecting the colors with the nearest distance to the given pixels.
 * This method also stores the indices of the corresponding pixels inside the
 * returned PaletteColor instances.
 */
public static List<PaletteColor> createInitialPalette(int pixels[], int[] palette){
    Map<Integer, Integer> used = new HashMap<>();
    ArrayList<PaletteColor> result = new ArrayList<>();

    for (int i = 0, l = pixels.length; i < l; i++) {
        double bestDistance = Double.MAX_VALUE;
        int bestIndex = -1;

        int pixel = pixels[i];
        int r1 = (pixel >> 16) & 0xFF;
        int g1 = (pixel >> 8) & 0xFF;
        int b1 = (pixel >> 0) & 0xFF;
        for (int k = 0; k < palette.length; k++) {
            int pixel2 = palette[k];
            int r2 = (pixel2 >> 16) & 0xFF;
            int g2 = (pixel2 >> 8) & 0xFF;
            int b2 = (pixel2 >> 0) & 0xFF;
            double dist = getPixelDistance(r1, g1, b1, r2, g2, b2);
            if (dist < bestDistance) {
                bestDistance = dist;
                bestIndex = k;
            }
        }

        Integer index = used.get(bestIndex);
        PaletteColor c;
        if (index == null) {
            index = result.size();
            c = new PaletteColor(palette[bestIndex]);
            result.add(c);
            used.put(bestIndex, index);
        } else{
            c = result.get(index);
        }
        c.indices.add(i);
    }
    return result;
}

/**
 * Creates a simple random color palette
 */
public static int[] createRandomColorPalette(int num_colors){
    Random random = new Random(101);

    int count = 0;
    int[] result = new int[num_colors];
    float add = 360f / (float)num_colors;
    for(float i = 0; i < 360f && count < num_colors; i += add) {
        float hue = i;
        float saturation = 90 +random.nextFloat() * 10;
        float brightness = 50 + random.nextFloat() * 10;
        result[count++] = Color.HSBtoRGB(hue, saturation, brightness);
    }
    return result;
}

public static int[] createGrayScalePalette(int count){
    float[] grays = new float[count];
    float step = 1f/(float)count;
    grays[0] = 0;
    for (int i = 1; i < count-1; i++) {
        grays[i]=i*step;
    }
    grays[count-1]=1;
    return createGrayScalePalette(grays);
}

/**
 * Returns a grayscale palette based on the given shades of gray
 */
public static int[] createGrayScalePalette(float[] grays){
    int[] result = new int[grays.length];
    for (int i = 0; i < result.length; i++) {
        float f = grays[i];
        result[i] = Color.HSBtoRGB(0, 0, f);
    }
    return result;
}


private static int[] createResultingImage(int[] pixels,List<PaletteColor> paletteColors, boolean dither, int w, int h) {
    int[] palette = new int[paletteColors.size()];
    for (int i = 0; i < palette.length; i++) {
        palette[i] = paletteColors.get(i).color;
    }
    if (!dither) {
        for (PaletteColor c : paletteColors) {
            for (int i : c.indices) {
                pixels[i] = c.color;
            }
        }
    } else{
        FloydSteinbergDither.generateDither(pixels, palette, w, h);
    }
    return palette;
}

public static int[] quantize(int[] pixels, int widht, int heigth, int[] colorPalette, int max_cols, boolean dither, ReductionStrategy reductionStrategy) {

    // create the initial palette by finding the best match colors from the given color palette
    List<PaletteColor> paletteColors = createInitialPalette(pixels, colorPalette);

    // reduce the palette size to the given number of maximum colors
    reducePalette(paletteColors, max_cols, reductionStrategy);
    assert paletteColors.size() <= max_cols;

    if (paletteColors.size() < max_cols) {
        // fill the palette with the nearest remaining colors
        List<PaletteColor> remainingColors = new ArrayList<>();
        Set<PaletteColor> used = new HashSet<>(paletteColors);
        for (int i = 0; i < colorPalette.length; i++) {
            int color = colorPalette[i];
            PaletteColor c = new PaletteColor(color);
            if (!used.contains(c)) {
                remainingColors.add(c);
            }
        }
        fillPalette(paletteColors, remainingColors, max_cols);
    }
    assert paletteColors.size() == max_cols;

    // create the resulting image
    return createResultingImage(pixels,paletteColors, dither, widht, heigth);

}   

static enum ReductionStrategy{
    ORIGINAL_COLORS,
    BETTER_CONTRAST,
    AVERAGE_DISTANCE,
}

public static void main(String args[]) throws IOException {

    // input parameters
    String imageFileName = args[0];
    File file = new File(imageFileName);

    boolean dither = true;
    int colorPaletteSize = 80;
    int max_cols = 3;
    max_cols =  Math.min(max_cols, colorPaletteSize);

    // create some random color palette
    //  int[] colorPalette = createRandomColorPalette(colorPaletteSize);
    int[] colorPalette = createGrayScalePalette(20);

    ReductionStrategy reductionStrategy = ReductionStrategy.AVERAGE_DISTANCE;

    // show the original image inside a frame
    ImageFrame original = new ImageFrame();
    original.setImage(file);
    original.setTitle("Original Image");
    original.setLocation(0, 0);

    Image image = original.getImage();
    int width = image.getWidth(null);
    int heigth = image.getHeight(null);
    int pixels[] = getPixels(image);
    int[] palette = quantize(pixels, width, heigth, colorPalette, max_cols, dither, reductionStrategy);

    // show the reduced image in another frame
    ImageFrame reduced = new ImageFrame();
    reduced.setImage(width, heigth, pixels);
    reduced.setTitle("Quantized Image (" + palette.length + " colors, dither: " + dither + ")");
    reduced.setLocation(100, 100);

}
}

ВОЗМОЖНЫЕ УЛУЧШЕНИЯ

1) Используемый алгоритм Флойда-Штайнберга в настоящее время работает только для палитр с максимальным размером 256. Я предполагаю, что это можно было бы легко исправить, но так как используемый класс FloydSteinbergDither требует довольно большого количества конверсий на данный момент, было бы лучше реализовать алгоритм с нуля, чтобы он соответствовал цветовой модели, которая используется в конце.

2) Я считаю, что использование другого алгоритма сглаживания, такого как scolorq, возможно, будет лучше. В "To Do List" в конце своей домашней страницы они пишут:

[TODO:] Возможность фиксировать некоторые цвета для заданного набора (поддерживается алгоритмом, но не текущей реализацией)

Таким образом, кажется, что использование алгоритма может быть выполнено с использованием фиксированной палитры. Плагин Photoshop/Gimp Ximagic, похоже, реализует эту функцию с помощью scolorq. На своей домашней странице:

Ximagic Quantizer - это плагин Photoshop для квантования цвета изображения (уменьшение цвета) и сглаживания. Предоставляет: Предопределенное квантование палитры

3) Возможно, алгоритм для заполнения палитры может быть улучшен. путем заполнения палитры цветами в зависимости от их среднего расстояния (например, в алгоритме сокращения). Но это должно быть проверено в зависимости от используемого алгоритма сглаживания.

Ответ 2

Прежде всего, я хотел бы настаивать на том, что это не расширенное вычисление цвета расстояния.

До сих пор я предполагал, что первая палитра - это одна из настроенных или , предварительно рассчитанная с изображения.

Здесь я только сконфигурировал его и сосредоточился на проблеме извлечения субпаллет. Я не использовал алгоритм, очень вероятно, что он может быть не лучшим.

  • Сохраните изображение в контексте canvas 2d, который будет служить в качестве буфера, я буду называть его ctxHidden
  • Сохранять данные пикселей ctxHidden в переменной с именем img
  • Проведите цикл через img с помощью функции constraintImageData(img, palette), которая принимает в качестве аргумента img и palette для преобразования текущих img пикселей в заданные цвета с помощью функции расстояния nearestColor(palette, r, g, b, a). Обратите внимание, что эта функция возвращает witness, которая в основном подсчитывает, сколько раз каждый цвет палитры используется хотя бы один раз. В моем примере также применяется сглаживание Floyd-Steinberg, хотя вы упомянули, что это не проблема.
  • Использование свидетеля для сортировки по убыванию по частоте появления видимости (из палитры)
  • Извлеките эти цвета из исходной палитры, чтобы получить субпалитку в соответствии с maxColors (или max_colors)
  • Нарисуйте изображение с последней субпальтой, начиная с ctxHidden исходных данных.

Вы должны ожидать, что ваше окончательное изображение даст вам слабые результаты, если maxColors слишком мала или ваша оригинальная палитра слишком далека от исходных цветов изображения.


Я сделал jsfiddle с processing.js, и здесь явно не нужно, но я начал использовать его, поэтому оставил его как есть.

Теперь вот что выглядит код (второй результат - результат, применяя окончательную субпалитту с задержкой в ​​3 секунды)

var image = document.getElementById('original'),
    palettePanel = document.getElementById('palette'),
    subPalettePanel = document.getElementById('subpalette'),
    canvas = document.getElementById('main'),
    maxColors = 12,
    palette = [
         0x7F8FB1FF,
         0x000000FF,
         0x404c00FF,
         0xe46501FF,
         0x722640FF,
         0x40337fFF,
         0x666666FF,
         0x0e5940FF,
         0x1bcb01FF,
         0xbfcc80FF,
         0x333333FF,
         0x0033CCFF,
         0x66CCFFFF,
         0xFF6600FF,
         0x000033FF,
         0xFFCC00FF,
         0xAA0033FF,
         0xFF00FFFF,
         0x00FFFFFF,
         0x123456FF
     ],
      nearestColor = function (palette, r, g, b, a) {
            var rr, gg, bb, aa, color, closest,
                distr, distg, distb, dista,
                dist,
                minDist = Infinity;
            for (var i =  0; i < l; i++) {
                color = palette[i];
                rr = palette[i] >> 24 & 0xFF;
                gg = palette[i] >> 16 & 0xFF;
                bb = palette[i] >> 8  & 0xFF;
                aa = palette[i]       & 0xFF;

                if (closest === undefined) {
                    closest = color;
                }

                // compute abs value
                distr = Math.abs(rr - r);
                distg = Math.abs(gg - g);
                distb = Math.abs(bb - b);
                dista = Math.abs(aa - a);
                dist = (distr + distg + distb + dista * .5) / 3.5;

                if (dist < minDist) {
                    closest = color;
                    minDist = dist;
                }
            }

            return closest; 
     },
     subpalette = [],
     i, l = palette.length,
     r, g, b, a,
     img,
     size = 5,
     cols = palettePanel.width / size,
     drawPalette = function (p, palette) {
            var i, l = palette.length;

            p.setup = function () {
            p.size(50,50);
            p.background(255);
            p.noStroke();

            for (i = 0; i < l; i++) {

                r = palette[i] >> 24 & 0xFF;
                g = palette[i] >> 16 & 0xFF;
                b = palette[i] >> 8  & 0xFF;
                a = palette[i]       & 0xFF;

                p.fill(r,g,b,a);
                p.rect (i%cols*size, ~~(i/cols)*size, size, size);
            }
        }
     },
        constraintImageDataToPalette = function (img, palette) {
            var i, l, x, y, index,
                pixel, x, y,
                right, bottom, bottomLeft, bottomRight,
                color,
                r, g, b, a, i, l,
                pr, pg, pb, pa,
                rErrorBase,
                gErrorBase,
                bErrorBase,
                aErrorBase,
                index,
                w = img.width,
                w4 = w*4,
                h = img.height,
                witness = {};

            for (i = 0, l = w*h*4; i < l; i += 4) {
                x = (i%w);
                y = ~~(i/w);
                index = x + y*w;
                right = index + 4,
                bottomLeft = index - 4 + w4,
                bottom = index + w4,
                bottomRight = index + w4 + 4,
                pixel = img.data;

                r = pixel[index];
                g = pixel[index+1];
                b = pixel[index+2];
                a = pixel[index+3];

                color = nearestColor(palette, r,g,b,a);
                witness[color] = (witness[color] || 0) + 1;

                // explode channels
                pr = color >> 24 & 0xFF;
                pg = color >> 16 & 0xFF;
                pb = color >> 8  & 0xFF;
                pa = color       & 0xFF;

                // set new color
                pixel[index]   = pr;
                pixel[index+1] = pg;
                pixel[index+2] = pb;
                pixel[index+3] = pa;

                // calculate error
                rErrorBase = (r - pr);
                gErrorBase = (g - pg);
                bErrorBase = (b - pb);
                aErrorBase = (a - pa);

                ///*
                // diffuse error right 7/16 = 0.4375
                pixel[right]   += 0.4375 * rErrorBase;
                pixel[right+1] += 0.4375 * gErrorBase;
                pixel[right+2] += 0.4375 * bErrorBase;
                pixel[right+3] += 0.4375 * aErrorBase;

                // diffuse error bottom-left 3/16 = 0.1875
                pixel[bottomLeft]   += 0.1875 * rErrorBase;
                pixel[bottomLeft+1] += 0.1875 * gErrorBase;
                pixel[bottomLeft+2] += 0.1875 * bErrorBase;
                pixel[bottomLeft+3] += 0.1875 * aErrorBase;

                // diffuse error bottom 5/16 = 0.3125
                pixel[bottom]   += 0.3125 * rErrorBase;
                pixel[bottom+1] += 0.3125 * gErrorBase;
                pixel[bottom+2] += 0.3125 * bErrorBase;
                pixel[bottom+3] += 0.3125 * aErrorBase;

                //diffuse error bottom-right 1/16 = 0.0625
                pixel[bottomRight]   += 0.0625 * rErrorBase;
                pixel[bottomRight+1] += 0.0625 * gErrorBase;
                pixel[bottomRight+2] += 0.0625 * bErrorBase;
                pixel[bottomRight+3] += 0.0625 * aErrorBase;
                //*/
            }
            return witness;
        };

new Processing(palettePanel, function (p) { drawPalette(p, palette); });

image.onload = function () {
        var l = palette.length;
    new Processing(canvas, function (p) {

        // argb 24 bits colors
        p.setup = function () {
            p.size(300, 200);
            p.background(0);
            p.noStroke();

            var ctx = canvas.getContext('2d'),
                ctxHidden = document.getElementById('buffer').getContext('2d'),
                img, log = [],
                witness = {};

            ctxHidden.drawImage(image, 0, 0);
            img = ctxHidden.getImageData(0, 0, canvas.width, canvas.height);

            // constraint colors to largest palette
            witness = constraintImageDataToPalette(img, palette);
            // show which colors have been picked from the panel
            new Processing(subPalettePanel, function (p) { drawPalette(p, Object.keys(witness)); });
            ctx.putImageData(img, 0, 0);

            var colorsWeights = [];

            for (var key in witness) {
                colorsWeights.push([+key, witness[key]]);
            }

            // sort descending colors by most presents ones
            colorsWeights.sort(function (a, b) {
                return b[1] - a[1];
            });

            // get the max_colors first of the colors picked to ensure a higher probability of getting a good color
            subpalette = colorsWeights
                .slice(0, maxColors)
                .map(function (colorValueCount) {
                    // return the actual color code
                    return colorValueCount[0];
            });

            // reset image we previously modified
            img = ctxHidden.getImageData(0, 0, canvas.width, canvas.height);
            // this time constraint with new subpalette
            constraintImageDataToPalette(img, subpalette);

            // wait 3 seconds to apply new palette and show exactly how it changed
            setTimeout(function () {
                new Processing(subPalettePanel, function (p) { drawPalette(p, subpalette); });
                ctx.putImageData(img, 0, 0);
            }, 3000);
        };
    });
};

ПРИМЕЧАНИЕ. У меня нет опыта вычисления java-изображений, поэтому я использовал javascript. Я попытался прокомментировать мой код, если у вас есть какие-либо вопросы по этому поводу, я отвечу и объясню это.

Ответ 3

РЕДАКТИРОВАТЬ: Я думаю, что, возможно, ответил на несколько иной вопрос. jarnbjo указал на то, что может быть неправильно с моим решением, и я понял, что неправильно понял вопрос. Однако я оставляю свой ответ здесь для потомков.

У меня может быть решение этого в Matlab. Чтобы найти ближайший цвет, я использовал весы, данные Альбертом Реншоу в комментарии здесь. Я использовал цветовое пространство HSV, но все входы в код были в стандартном RGB. Ярлыки с оттенками серого были преобразованы в трехканальные изображения в оттенках серого.

Чтобы выбрать наилучшие цвета для использования, я посеял kmeans с палитрой тестового образца, а затем reset центроиды были значениями, которые были ближе всего к палитре с образцом.

function imo = recolor(im,new_colors,max_colors)

% Convert to HSV
im2 = rgb2hsv(im);
new_colors = rgb2hsv(new_colors);

% Get number of colors in palette
num_colors = uint8(size(new_colors,1));

% Reshape image so every row is a diferent pixel, and every column a channel
% this is necessary for kmeans in Matlab
im2 = reshape(im2, size(im,1)*size(im,2),size(im,3));

% Seed kmeans with sample pallet, drop empty clusters
[IDX, C] = kmeans(im2,max_colors,'emptyaction','drop');

% For each pixel, IDX tells which cluster in C it corresponds to
% C contains the centroids of each cluster


% Because centroids are adjusted from seeds, we need to select which original color
% in the palette it corresponds to. We cannot be sure that the centroids in C correspond
% to their seed values
% Note that Matlab starts indexing at 1 instead of 0
for i=1:size(C,1)
    H = C(i,1);
    S = C(i,2);
    V = C(i,3);
    bdel = 100;
    % Find which color in the new_colors palette is closest
    for j=1:size(new_colors,1)
        H2 = new_colors(j,1);
        S2 = new_colors(j,2);
        V2 = new_colors(j,3);
        dH = (H2-H)^2*0.475;
        dS = (S2-S)^2*0.2875;
        dV = (V2-V)^2*0.2375;
        del = sqrt(dH+dS+dV);
        if isnan(del)
            continue
        end
        % update if the new delta is lower than the best
        if del<bdel
            bdel = del;
            C(i,:) = new_colors(j,:);
        end
    end
end

% Update the colors, this is equal to the following
% for i=1:length(imo)
%    imo(i,:) = C(IDX(i),:) 
imo = C(IDX,:);

% put it back in its original shape
imo = reshape(imo, size(im));

imo = hsv2rgb(imo);

imshow(imo);

Проблема с этим сейчас, как я уже писал, заключается в том, что для цветных изображений очень медленно (Lenna занимает несколько минут).

Является ли это тем, что вы ищете?

Примеры.

Если вы не понимаете все обозначения Matlab, дайте мне знать.

Ответ 4

Ниже представлен подход, реализованный в Java, с использованием Marvin Framework. Это может быть отправной точкой для решения вашей проблемы.

Ввод:

  • Палитра P с цветами M.
  • Число цветов N.
  • Изображение G

Шаги:

  • Примените палитру P к изображению G, заменив цвет пикселей на самый похожий цвет (меньшее расстояние в пространстве RGB) в палитре. Выходное изображение имеет распределение цветов палитры по использованию.
  • Вычислить гистограмму, содержащую каждый цвет в палитре, и сколько раз она используется в изображении (количество пикселей).
  • Сортировка палитры по использованию пикселов, большинство из которых менее используются.
  • Выберите N первые элементы в отсортированном списке и создайте новую палитру.
  • Примените эту новую палитру к изображению.

Ниже представлен результат этого подхода.

Исходное изображение:

http://marvinproject.sourceforge.net/other/lena.jpg

Палитра, а изображение количественно с 32, 8, 4 цветами:

enter image description here

Исходный код:

public class ColorQuantizationExample {

    public ColorQuantizationExample(){
        MarvinImage imageOriginal = MarvinImageIO.loadImage("./res/quantization/lena.jpg");
        MarvinImage imageOutput = new MarvinImage(imageOriginal.getWidth(), imageOriginal.getHeight());

        Set<Color> palette = loadPalette("./res/quantization/palette_7.png");
        quantitize(imageOriginal, imageOutput, palette, 32);
        MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_7_32.jpg");

        quantitize(imageOriginal, imageOutput, palette, 8);
        MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_7_8.jpg");

        quantitize(imageOriginal, imageOutput, palette, 4);
        MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_7_4.jpg");

        palette = loadPalette("./res/quantization/palette_8.png");
        quantitize(imageOriginal, imageOutput, palette, 32);
        MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_8_32.jpg");

        quantitize(imageOriginal, imageOutput, palette, 8);
        MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_8_8.jpg");

        quantitize(imageOriginal, imageOutput, palette, 4);
        MarvinImageIO.saveImage(imageOutput, "./res/quantization/lena_8_4.jpg");
    }

    /**
     * Load a set of colors from a palette image.
     */
    private Set<Color> loadPalette(String path){
        Set<Color> ret = new HashSet<Color>();
        MarvinImage image = MarvinImageIO.loadImage(path);
        String key;
        for(int y=0; y<image.getHeight(); y++){
            for(int x=0; x<image.getWidth(); x++){
                Color c = new Color
                (
                    image.getIntComponent0(x, y),
                    image.getIntComponent1(x, y),
                    image.getIntComponent2(x, y)
                );

                ret.add(c);
            }
        }
        return ret;
    }

    private void quantitize(MarvinImage imageIn, MarvinImage imageOut, Set<Color> palette, int colors){
        applyPalette(imageIn, imageOut, palette);
        HashMap<Color, Integer> hist = getColorHistogram(imageOut);


        List<Map.Entry<Color, Integer>> list = new LinkedList<Map.Entry<Color, Integer>>( hist.entrySet() );

        Collections.sort( list, new Comparator<Map.Entry<Color, Integer>>()
        {
            @Override
            public int compare( Map.Entry<Color, Integer> o1, Map.Entry<Color, Integer> o2 )
            {
                return (o1.getValue() > o2.getValue() ? -1: 1);
            }
        } );

        Set<Color> newPalette = reducedPalette(list, colors);
        applyPalette(imageOut.clone(), imageOut, newPalette);
    }

    /**
     * Apply a palette to an image.
     */
    private void applyPalette(MarvinImage imageIn, MarvinImage imageOut, Set<Color> palette){
        Color color;
        for(int y=0; y<imageIn.getHeight(); y++){
            for(int x=0; x<imageIn.getWidth(); x++){
                int red = imageIn.getIntComponent0(x, y);
                int green = imageIn.getIntComponent1(x, y);
                int blue = imageIn.getIntComponent2(x, y);

                color = getNearestColor(red, green, blue, palette);
                imageOut.setIntColor(x, y, 255, color.getRed(), color.getGreen(), color.getBlue());
            }
        }
    }

    /**
     * Reduce the palette colors to a given number. The list is sorted by usage.
     */
    private Set<Color> reducedPalette(List<Map.Entry<Color, Integer>> palette, int colors){
        Set<Color> ret = new HashSet<Color>();
        for(int i=0; i<colors; i++){
            ret.add(palette.get(i).getKey());
        }
        return ret;
    }

    /**
     * Compute color histogram
     */
    private HashMap<Color, Integer> getColorHistogram(MarvinImage image){
        HashMap<Color, Integer> ret = new HashMap<Color, Integer>();

        for(int y=0; y<image.getHeight(); y++){
            for(int x=0; x<image.getWidth(); x++){
                Color c = new Color
                (
                    image.getIntComponent0(x, y),
                    image.getIntComponent1(x, y),
                    image.getIntComponent2(x, y)
                );

                if(ret.get(c) == null){
                    ret.put(c, 0);
                }
                ret.put(c, ret.get(c)+1);
            }
        }
        return ret;
    }

    private Color getNearestColor(int red, int green, int blue, Set<Color> palette){
        Color nearestColor=null, c;
        double nearestDistance=Integer.MAX_VALUE;
        double tempDist;
        Iterator<Color> it = palette.iterator();

        while(it.hasNext()){
            c = it.next();
            tempDist = distance(red, green, blue, c.getRed(), c.getGreen(), c.getBlue());
            if(tempDist < nearestDistance){
                nearestDistance = tempDist;
                nearestColor = c;
            }
        }
        return nearestColor;
    }

    private double distance(int r1, int g1, int b1, int r2, int g2, int b2){
        double dist= Math.pow(r1-r2,2) + Math.pow(g1-g2,2) + Math.pow(b1-b2,2);
        return Math.sqrt(dist);
    }

    public static void main(String args[]){
        new ColorQuantizationExample();
    }
}