使用N个预定义颜色中的N进行颜色量化

时间:2014-01-31 04:08:01

标签: java image-processing color-palette

我在尝试量化和抖动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问到,我也可以用这个练习解释目标,尽管它与如何解决实际问题无关。输出图像的目标是刺绣或挂毯图案。在最简单的情况下,输出图像中的每个像素对应于在某种载体织物上制作的针脚。调色板对应于可用的纱线,通常有几百种颜色。然而,出于实际原因,必须限制实际工作中使用的颜色数量。谷歌搜索gobelin刺绣将举几个例子。

并澄清问题究竟在哪里......解决方案确实可以分为两个单独的步骤:

  • 选择原始调色板的最佳子集
  • 使用子集渲染输出图像

这里,第一步是实际问题。如果调色板选择正常,我可以简单地使用所选择的颜色,例如Floyd-Steinberg犹豫不决以产生合理的结果(实施起来相当简单)。

如果我正确理解了scolorq的实现,scolorq结合了这两个步骤,使用调色板选择中的抖动算法知识来创建更好的结果。这当然是一个首选的解决方案,但scolorq中使用的算法稍微超出了我的数学知识。

4 个答案:

答案 0 :(得分:5)

<强>概览

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

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

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

3)如果生成的调色板小于允许的最大颜色数,则会从输入调色板的其余颜色中填充最相似的颜色,直到达到允许的颜色数。这是希望抖动算法在抖动期间可以使用这些颜色。请注意,虽然我没有看到填充或未填充Floyd-Steinberg算法的调色板之间存在很大差异...

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


<强>实施

以下是此方法的实现。

如果要运行源代码,则需要此类: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类目前需要进行大量的转换,因此从头开始实现算法肯定会更好,因此它最适合最终使用的颜色模型。 p>

2)我相信使用像scolorq这样的另一种抖动算法可能会更好。在&#34;待办事项清单&#34;在他们的主页末尾他们写道:

  

[TODO:]将某些颜色固定到预定集合的能力(由算法支持但不支持当前实现)

所以似乎使用固定的调色板应该可以用于算法。 Photoshop / Gimp插件Ximagic似乎使用scolorq实现此功能。从他们的主页:

  

Ximagic Quantizer是一款用于图像颜色量化(色彩还原)的Photoshop插件。抖动。   提供:预定义的调色板量化

3)填充调色板的算法也许可以改进 - 例如通过根据平均距离填充调色板(如缩小算法)。但这应该根据最终使用的抖动算法进行测试。

答案 1 :(得分:0)

首先,我想坚持这不是高级距离颜色计算的事实。

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

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

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

  9. 如果processing.js太低或原始调色板与原始图像颜色相距太远,您必须期望最终图像能给您带来柔和的效果。


    I did a jsfiddle 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); }; }); }; ,显然不是必要,但我开始使用它,所以我保持原样。

    现在这里是代码的样子(第二个画布是结果,应用最终子选板延迟3秒)

    {{1}}

    注意:我没有java图像计算经验,所以我使用了javascript代替。我试着评论我的代码,如果你对此有任何疑问,我会回答并解释它。

答案 2 :(得分:0)

编辑:我想我可能已经回答了一个稍微不同的问题。 jarnbjo指出了我的解决方案可能有问题的东西,我意识到我误解了这个问题。不过,我现在就把这个答案留给后人了。

我可以在Matlab中找到解决方案。为了找到最接近的颜色,我使用了Albert Renshaw在评论here中给出的权重。我使用了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需要几分钟)。

这是否符合您的要求?

Examples

如果您不理解所有Matlab符号,请告诉我。

答案 3 :(得分:0)

下面介绍了使用Marvin Framework在Java中实现的方法。这可能是解决问题的起点。

<强>输入:

  • 调色板 P M 颜色。
  • 颜色数 N
  • 图片 G

<强>步骤:

  1. 通过将调色板中的像素颜色替换为最相似的颜色(RGB空间中的距离较小),将调色板 P 应用于图像 G 。输出图像按使用情况分配调色板颜色。
  2. 计算包含调色板中每种颜色的直方图以及在图像中使用的次数(像素数)。
  3. 按像素使用情况对调色板进行排序,大部分用于较少使用。
  4. 选择排序列表中的 N 第一项并生成新的调色板。
  5. 将此新调色板应用于图像。
  6. 下面是这种方法的输出。

    原始图片:

    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();
        }
    }