Как создать X% процентов серого цвета в Java?

Предположим, я хочу 25% или 31% серого цвета в Java?

Следующий код показывает

    BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_BYTE_GRAY);

    image.setRGB(0, 0, new Color(0,0,0).getRGB());
    image.setRGB(1, 0, new Color(50, 50, 50).getRGB());
    image.setRGB(0, 1, new Color(100,100,100).getRGB());
    image.setRGB(1, 1, new Color(255,255,255).getRGB());

    Raster raster = image.getData();
    double[] data = raster.getPixels(0, 0, raster.getWidth(), raster.getHeight(), (double[]) null);

    System.out.println(Arrays.toString(data));

Очевидный факт, что RGC связан с плотностью (?) нелинейной

[0.0, 8.0, 32.0, 255.0]

Итак, как создать цвет заданной плотности?

ОБНОВИТЬ

Я попробовал методы, предложенные @icza и @hlg, а также еще один найденный мной:

    double[] data;
    Raster raster;
    BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY);

    float[] grays = {0, 0.25f, 0.5f, 0.75f, 1};

    ColorSpace linearRGB = ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB);
    ColorSpace GRAY = ColorSpace.getInstance(ColorSpace.CS_GRAY);

    Color color;
    int[] rgb;

    for(int i=0; i<grays.length; ++i) {

        System.out.println("\n\nShould be " + (grays[i]*100) + "% gray");

        color = new Color(linearRGB, new float[] {grays[i], grays[i], grays[i]}, 1f);

        image.setRGB(0, 0, color.getRGB());
        raster = image.getData();
        data = raster.getPixels(0, 0, 1, 1, (double[]) null);

        System.out.println("data by CS_LINEAR_RGB (hlg method) = " + Arrays.toString(data));

        color = new Color(GRAY, new float[] {grays[i]}, 1f);

        image.setRGB(0, 0, color.getRGB());
        raster = image.getData();
        data = raster.getPixels(0, 0, 1, 1, (double[]) null);

        System.out.println("data by CS_GRAY = " + Arrays.toString(data));

        rgb = getRGB(Math.round(grays[i]*255));

        color = new Color(rgb[0], rgb[1], rgb[2]);

        image.setRGB(0, 0, color.getRGB());
        raster = image.getData();
        data = raster.getPixels(0, 0, 1, 1, (double[]) null);

        System.out.println("data by icza method = " + Arrays.toString(data));

    }

и все дали разные результаты!

Should be 0.0% gray
data by CS_LINEAR_RGB (hlg method) = [0.0]
data by CS_GRAY = [0.0]
data by icza method = [0.0]


Should be 25.0% gray
data by CS_LINEAR_RGB (hlg method) = [63.0]
data by CS_GRAY = [64.0]
data by icza method = [36.0]


Should be 50.0% gray
data by CS_LINEAR_RGB (hlg method) = [127.0]
data by CS_GRAY = [128.0]
data by icza method = [72.0]


Should be 75.0% gray
data by CS_LINEAR_RGB (hlg method) = [190.0]
data by CS_GRAY = [192.0]
data by icza method = [154.0]


Should be 100.0% gray
data by CS_LINEAR_RGB (hlg method) = [254.0]
data by CS_GRAY = [254.0]
data by icza method = [255.0]

Теперь интересно, какой из них правильный?

ОБНОВЛЕНИЕ 2

Извините, серый / белый процент должен быть, конечно, полностью изменен.

3 ответа

Решение

Огромные различия связаны с гамма-кодированием в sRGB ( Википедия). sRGB - это цветовое пространство по умолчанию, используемое в Color конструктор. Если вы устанавливаете цвета с использованием линейного цветового пространства RGB, значения серого не искажаются:

ColorSpace linearRGB = ColorSpace.getInstance(ColorSpace.CS_LINEAR_RGB);
Color grey50 = new Color(linearRGB, new float[]{50f/255,50f/255,50f/255}, 1f);
Color grey100 = new Color(linearRGB, new float[]{100f/255,100f/255,100f/255}, 1f);
Color grey255 = new Color(linearRGB, new float[]{1f,1f,1f}, 1f);

Однако при настройке пикселя с помощью Color.getRGB а также ImageBuffer.setRGB, линейные значения серой шкалы преобразуются в sRGB и обратно. Таким образом, они гамма-кодируются и декодируются, что приводит к ошибкам округления в зависимости от выбранного цветового пространства.

Этих ошибок можно избежать, установив необработанные данные пикселей непосредственно за цветовую модель серой шкалы:

WritableRaster writable = image.getRaster();
writable.setPixel(0,0, new int[]{64});

Обратите внимание, что вы должны округлить процентные значения, например, для 25% вы не можете сохранить 63.75, Если вам нужно больше точности, используйте TYPE_USHORT_GRAY вместо TYPE_BYTE_GRAY,

При преобразовании цвета RGB в оттенки серого используются следующие весовые коэффициенты:

0,2989, 0,5870, 0,1140

Источник: преобразование RGB в оттенки серого / интенсивность

И в Википедии: http://en.wikipedia.org/wiki/Grayscale

Итак, формально

gray = 0.2989*R + 0.5870*G + 0.1140*B

В основном, вам нужно обратное этой функции. Вам нужно найти значения R, G и B, которые дают результат gray ценность, которую вы ищете. Поскольку в уравнении есть 3 параметра, в большинстве случаев имеется много значений RGB, что приведет к gray ценность, которую вы ищете.

Только подумайте: цвет RGB с высоким компонентом R и ни один из G и B не дает серый цвет, может быть другой цвет RGB с некоторым компонентом G, и ни один из цветов R и B не дает тот же серый цвет, так что существует несколько возможных RGB-решения нужного серого цвета.

Алгоритм

Вот одно из возможных решений. Что он делает, так это пытается установить первый из компонентов RGB настолько большим, что умножение на его вес вернет gray, Если он "переполняется" за пределы 255, он сокращается, мы уменьшаем gray с суммой, которую максимальное значение компонента может "представлять", и мы пытаемся сделать это для следующего компонента с оставшимися gray количество.

Здесь я использую gray диапазон ввода 0..255, Если вы хотите указать его в процентах, просто конвертируйте его как gray = 255*percent/100,

private static double[] WEIGHTS = { 0.2989, 0.5870, 0.1140 };

public static int[] getRGB(int gray) {
    int[] rgb = new int[3];

    for (int i = 0; i < 3; i++) {
        rgb[i] = (int) (gray / WEIGHTS[i]);
        if (rgb[i] < 256)
            return rgb; // Successfully "distributed" all of gray, return it

        // Not quite there, cut it...
        rgb[i] = 255;
        // And distribute the remaining on the rest of the RGB components:
        gray -= (int) (255 * WEIGHTS[i]);
    }

    return rgb;
}

Чтобы проверить это, используйте следующий метод:

public static int toGray(int[] rgb) {
    double gray = 0;
    for (int i = 0; i < 3; i++)
        gray += rgb[i] * WEIGHTS[i];
    return (int) gray;
}

Тестовое задание:

for (int gray = 0; gray <= 255; gray += 50) {
    int[] rgb = getRGB(gray);
    System.out.printf("Input: %3d, Output: %3d,   RGB: %3d, %3d, %3d\n",
            gray, toGray(rgb), rgb[0], rgb[1], rgb[2]);
}

Тестовый вывод:

Input:   0, Output:   0,   RGB:   0,   0,   0
Input:  50, Output:  49,   RGB: 167,   0,   0
Input: 100, Output:  99,   RGB: 255,  40,   0
Input: 150, Output: 150,   RGB: 255, 126,   0
Input: 200, Output: 200,   RGB: 255, 211,   0
Input: 250, Output: 250,   RGB: 255, 255, 219

Результаты показывают, что мы ожидали, основываясь на алгоритме: компонент R "заполняется" первым, когда он достигает 255, компонент G "заполняется" и, наконец, компонент G используется.

Цвет имеет определенную яркость, которую вы хотите сохранить, если цвет более серый.

Яркость может быть что-то вроде:

Y = 0.2989*R + 0.5870*G + 0.1140*B
Y = 0.2126*R + 0.7152*G + 0.0722*B

Так new Color(Y, Y, Y) соответствует значению серого с той же яркостью. Седой до определенного процента - это интерполяция.

Color grayed(Color color, int perc) {
    double percGrayed = perc / 100.0;
    double percColored = 1.0 - percGrayed;

    double[] weights = { 0.2989, 0.5870, 0.1140 };
    double[] rgb = { color.getR(), color.getG(), color.getB() };

    // Determine luminance:
    double y = 0.0;
    for (int i = 0; i < 3; ++i) {
        y += weights[i] * rgb[i];
    }

    // Interpolate between (R, G, B) and (Y, Y, Y):
    for (int i = 0; i < 3; ++i) {
        rgb[i] *= percColoured;
        rgb[i] += y * percGrayed;
    }

    return new Color((int)rgb[0], (int)rgb[1], (int)rgb[2]);
}

Color grayedColor = grayed(color, 30); // 30% grayed.
Другие вопросы по тегам