N个预定义颜色的M种颜色量化

17
我正在尝试量化和抖动RGB图像,遇到了一些奇怪的问题。理想情况下,我应该能够在Java中实现适当的算法或使用Java库,但其他语言实现的参考也可能有所帮助。
以下作为输入:
- image:24位RGB位图 - palette:使用其RGB值定义的颜色列表 - max_cols:输出图像中要使用的最大颜色数
重要的是,调色板的大小以及允许的最大颜色数不一定是2的幂,并且可能大于255。
因此,目标是获取image,从提供的palette中选择最多max_cols个颜色,并仅使用选定的颜色进行渲染,使用某种误差扩散抖动。使用哪种抖动算法并不重要,但它应该是一个误差扩散变体(例如Floyd-Steinberg),而不是简单的半色调或有序抖动。

性能并不是特别重要的因素,而且预期数据输入的大小相对较小。图像很少会大于500x500像素,提供的调色板可能包含一些3-400种颜色,颜色数量通常会限制在100种以下。还可以安全地假设调色板包含广泛的颜色选择,涵盖了色相、饱和度和亮度的变化。

scolorq使用的调色板选择和抖动效果非常理想,但似乎难以将该算法适应于从已定义的调色板中选择颜色而不是任意颜色。

更准确地说,我遇到的问题是从提供的调色板中选择合适的颜色。假设我使用scolorq创建具有N个颜色的调色板,然后用提供调色板中最接近的颜色替换由scolorq定义的颜色,并将这些颜色与误差扩散抖动结合使用。这将产生至少类似于输入图像的结果,但由于所选颜色的不可预测色调,输出图像可能会产生强烈的、不希望的色调。例如,当使用灰度输入图像和仅具有少量中性灰色调而具有大范围棕色调(或更一般地说,许多具有相同色调、低饱和度和亮度变化的颜色)的调色板时,我的颜色选择算法似乎更喜欢这些颜色而不是中性灰色,因为棕色调至少在数学上更接近所需颜色而不是灰调。即使我将RGB值转换为HSB并在尝试查找最接近的可用颜色时使用不同的H、S和B通道权重,同样的问题仍然存在。

有没有建议如何正确实现这个功能,或者更好的一个库可以用来执行任务?

由于Xabster的提问,我可以解释一下这个练习的目标,尽管它与如何解决实际问题无关。输出图像的目标是刺绣或挂毯图案。在最简单的情况下,输出图像中的每个像素对应于在某种载体织物上制作的一针。调色板对应于可用的纱线,通常有几百种颜色。出于实际原因,有必要限制实际工作中使用的颜色数量。搜索哥伯林刺绣将给出几个例子。
为了澄清问题的确切所在...解决方案确实可以分为两个单独的步骤:
- 选择原始调色板的最佳子集 - 使用子集来呈现输出图像
在这里,第一步是实际的问题。如果调色板选择正常工作,我可以简单地使用选定的颜色和例如Floyd-Steinberg抖动来产生合理的结果(这是相当容易实现的)。
如果我正确理解scolorq的实现,scolorq将这两个步骤结合起来,利用调色板选择中抖动算法的知识来创建更好的结果。当然,这将是首选的解决方案,但scolorq中使用的算法略微超出了我的数学知识。

@Elliot:除非我漏掉了什么,OrdererdDitherDescriptor只会执行抖动而不是调色板选择(这正是我卡住的问题)。如果我找到了一个合理的方法来选择要使用的调色板,则实现抖动而不使用JAI相当容易。 - jarnbjo
@Xabster:那至少是问题的一部分。然而,主要问题在于如何选择可用调色板的最佳子集,以便输出图像可以尽可能接近输入图像进行渲染。正如我在问题中试图解释的那样,仅仅选择与图像中使用的颜色接近的调色板条目并不特别有效。 - jarnbjo
2
好的,所以这既是选择可用颜色整个调色板子集的问题,也是确定每个替换使用哪种颜色的问题。选择调色板子集的目标是什么?压缩吗? - Xabster
@Xabster:这个问题对于评论来说太多了,但我已经编辑了我的问题,试图在那里回答你的问题。 - jarnbjo
你还没有看过这个吧? - Hannes
显示剩余8条评论
4个回答

7

概览

这是解决问题的一种可能方法:

1)将输入像素中的每个颜色映射到输入调色板中最接近的颜色。

2)如果生成的调色板大于允许的最大颜色数量,则通过从计算出的调色板中删除彼此最相似的颜色来将调色板减少到最大允许数量(我选择了最近距离进行删除,因此结果图像保持高对比度)。

3)如果生成的调色板小于允许的最大颜色数量,则使用剩余输入调色板中最相似的颜色填充调色板,直到达到允许的颜色数量。希望dithering算法在dithering期间可以利用这些颜色。请注意,我没有看到用Floyd-Steinberg算法填充或不填充调色板之间有太大的区别...

4)作为最后一步,输入像素使用计算得出的调色板进行dithering。


实现

以下是该方法的实现。

如果要运行源代码,则需要这个类:ImageFrame.java。您可以将输入图像设置为唯一的程序参数,所有其他参数必须在main方法中设置。使用的Floyd-Steinberg算法来自Floyd-Steinberg dithering

可以在调色板减少算法中选择3种不同的减少策略:

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)目前使用的Floyd-Steinberg算法仅适用于最多具有256种颜色的调色板。我想这可能很容易修复,但由于目前使用的FloydSteinbergDither类需要相当多的转换,因此最好从头实现算法,以便其适合最后使用的颜色模型。

2)我认为像scolorq这样使用另一种抖动算法可能更好。在他们主页的“待办事项”清单中,他们写道:

[待办:]将一些颜色固定为预定集(该算法支持但当前未实现)的能力

因此,似乎可以对算法使用固定的调色板。Photoshop/Gimp插件Ximagic似乎使用scolorq实现了这个功能。从他们的首页上:

Ximagic Quantizer是用于图像颜色量化(颜色减少)和抖动的Photoshop插件。 提供:预定义的调色板量化

3)填充调色板的算法可能需要改进 - 例如通过使用平均距离(如在减少算法中)来填充调色板。但这应该根据最终使用的抖动算法进行测试。


你的第一点是正确的。如果我可以使用整个调色板,按照你描述的过程,我会得到一个很好的结果。然而,你的第二点是错误的。即使我将第一步的结果作为“正常”的量化和抖动处理(例如scolorq)的输入,输出也不能保证仅使用原始调色板中的颜色。scolorq(以及基于聚类的量化算法)不会使用输入图像中颜色的确切子集,而是可能使用其他更适合呈现图像的颜色。 - jarnbjo
这看起来非常有前途。如果我没有发现任何问题,我会进行进一步测试并接受这个答案。 - jarnbjo

0

首先我想强调的是,这不是高级的距离颜色计算。

到目前为止,我假设第一个调色板是你要么配置,要么从图像预先计算的调色板。

在这里,我只配置了它,并专注于子调色板提取问题。我没有使用算法,很可能并不是最好的算法

  1. 将图像存储到2D画布上下文中,作为缓冲区,我将其称为ctxHidden
  2. ctxHidden的像素数据存储到名为img的变量中
  3. 使用函数constraintImageData(img, palette)循环遍历整个img,该函数接受imgpalette作为参数,以帮助距离函数nearestColor(palette, r, g, b, a)将当前img像素转换为给定颜色。请注意,此函数返回一个witness,它基本上计算调色板中每种颜色被使用至少一次的次数。我的示例还应用了Floyd-Steinberg抖动,即使您提到它不是问题。
  4. 使用witness按颜色出现频率(来自调色板)进行降序排序
  5. 从初始调色板中提取这些颜色,以根据maxColors(或max_colors)获取子调色板
  6. 使用最终子调色板从ctxHidden原始数据绘制图像。

如果maxColors太低或者原始调色板与原始图像颜色相差太远,你必须预期最终图像会产生模糊的效果。


我用processing.js做了一个jsfiddle,显然这里并不是必要的,但我开始使用它,所以就保留了下来。

现在这里是代码的样子(第二个画布是结果,应用最终的子调色板,并延迟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。我尽量在代码中添加注释,如果您对此有任何疑问,我会回答并解释。


这种方法基本上与Gabriel Archanjo的答案相同,因此我在他的答案中评论的问题也适用于这个解决方案。 - jarnbjo
如果您首先应用抖动,然后从生成的像素计算最频繁的颜色,最后再应用新调色板,而不需要另一次抖动,那该怎么办? - axelduch
请尝试使用白黑渐变,一个包含从白色到黑色的四个灰度的调色板,然后将颜色数量减少到两个。这样做不会起作用。 - jarnbjo

0

编辑:我觉得我可能回答的是一个稍微不同的问题。jarnbjo指出了我的解决方案可能存在问题,我意识到我误解了问题。尽管如此,我还是把我的答案留在这里供后人参考。

我可能在Matlab中找到了一个解决方案。为了找到最接近的颜色,我使用了Albert Renshaw在评论这里中给出的权重。我使用了HSV颜色空间,但代码中的所有输入都是标准RGB。灰度图像被转换为3通道的灰度图像。

为了选择要使用的最佳颜色,我使用测试样本调色板初始化了kmeans,然后将质心重置为样本调色板中最接近它们的值。

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符号,请告诉我。


老实说,我对Matlab的符号表示一点也不了解,但我觉得我应该能够理解它。然而,我想知道的是,在哪里定义了max_cols,即要从调色板中选择的颜色数量? - jarnbjo
没错,这可能是我的解决方案存在的问题。当我编写它时,我使用了max_colors = colors_in_palette。虽然我认为它仍然可以工作,但我们将不再使用kmeans进行种子处理。这应该没问题,因为最终产品中的颜色数可以少于max_colors。将max_colors添加到参数列表中,并将"[IDX,C] = kmeans(im2,num_colors,'start',new_colors,'emptyaction','drop');"更改为"[IDX,C] = kmeans(im2,max_colors,'emptyaction','drop');"。现在应该可以工作了。我已经使用更改重新测试了我的代码,看起来还不错。 - wbest
你可能需要确保你使用的kmeans库具有删除空聚类的功能。我进行了快速搜索,并发现UMass开发了一个kmeans,可以实现这一点。 - wbest
你是对的。但这样会行吗?假设输入图像是由白色到黑色的线性灰度梯度,基本调色板包含四个中性灰色调,其值为[0.0,0.4,0.6,1.0],且可以使用两个颜色。那么集群质心不会计算为0.25和0.75,并选择0.4和0.6这两种颜色吗?考虑到需要抖动输出的要求,与另外两种颜色0.0和1.0相比,这两种颜色远不适合用于重现一个体面的输出。 - jarnbjo
根据kmeans理论(不是我的代码),0.25和0.75并不是保证值。当我在我的灰度图像上运行它时,我选择的图像的质心为0.1和0.6。所以在这种情况下,你是错的。然而,在其他情况下,质心可能是0.25和0.75。因此,不,你没有正确理解它,因为它会因图像而异。一些灰度图像将具有0.25和0.75的质心,而其他图像则不会。 - wbest
显示剩余7条评论

0

以下是使用Marvin Framework实现的Java方法。它可能是解决您问题的起点。

输入:

  • 调色板P,具有M种颜色。
  • 颜色数量N
  • 图像G

步骤:

  1. 通过将像素颜色替换为调色板中最相似的颜色(在RGB空间中距离较小)来将调色板P应用于图像G。输出图像具有按使用情况分布的调色板颜色。
  2. 计算包含调色板中每种颜色及其在图像中使用次数(像素数)的直方图。
  3. 按像素使用情况从多到少对调色板进行排序。
  4. 选择排序列表中的前N个项目并生成新的调色板。
  5. 将此新调色板应用于图像。

以下是此方法的输出。

原始图像:


(来源:sourceforge.net

调色板,以及使用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();
    }
}

我认为我对wbest的答案发表的评论也适用于这个解决方案。假设输入图像除了白黑渐变外没有任何内容,调色板具有灰度[0.0、0.33、0.67、1.0],且限制只使用两种颜色。如果使用来自完整调色板的“最近选择”将图像量化,则会导致颜色0.33和0.67被使用两次,因此被用于进一步缩减。在这种情况下,考虑到使用抖动,颜色0.0和1.0更适合呈现输出图像。 - jarnbjo
即使调色板减少和抖动可以分为两个独立的步骤,我认为调色板减少算法至少必须考虑到抖动将要发生。最佳子集显然取决于是否使用抖动。 - jarnbjo
你说得完全正确。也许,在第一步中执行抖动是必要的。分析抖动中最常用的颜色组合,计算单个颜色(或它们的组合)的直方图,并使用这些新信息进行量化。 - Gabriel Archanjo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接