Как создать 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.