Преобразование изображения TYPE_INT_RGB в изображение TYPE_BYTE_GRAY приводит к неверному результату - PullRequest
2 голосов
/ 04 мая 2020

Я пытаюсь преобразовать изображение в градациях серого в 24-битном формате RGB в изображение в градациях серого в 8-битном формате. Другими словами, вход и выход должны быть визуально идентичны, меняется только количество каналов. Вот входное изображение:

input

Код, используемый для преобразования его в 8-битный:

File input = new File("input.jpg");
File output = new File("output.jpg");

// Read 24-bit RGB input JPEG.
BufferedImage rgbImage = ImageIO.read(input);
int w = rgbImage.getWidth();
int h = rgbImage.getHeight();

// Create 8-bit gray output image from input.
BufferedImage grayImage = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
int[] rgbArray = rgbImage.getRGB(0, 0, w, h, null, 0, w);
grayImage.setRGB(0, 0, w, h, rgbArray, 0, w);

// Save output.
ImageIO.write(grayImage, "jpg", output);

А вот выходное изображение:

output

Как видите, есть небольшая разница. Но они должны быть идентичными. Для тех, кто этого не видит, вот разница между двумя изображениями (при просмотре в режиме смешивания Разница в Gimp, полный черный цвет не будет указывать на разницу). Та же проблема возникает, если я вместо этого использую PNG для ввода и вывода.

После выполнения grayImage.setRGB я попытался сравнить значения цвета для одного и того же пикселя на обоих изображениях:

int color1 = rgbImage.getRGB(230, 150);  // This returns 0xFF6D6D6D.
int color2 = grayImage.getRGB(230, 150);  // This also returns 0xFF6D6D6D.

Одинаковый цвет для обоих. Однако, если я сделаю то же самое сравнение с изображениями в Gimp, я получу 0xFF6D6D6D и 0xFF272727 соответственно ... огромная разница.

Что здесь происходит? Есть ли способ получить идентичное 8-битное изображение из 24-битного изображения в градациях серого? Я использую Oracle JDK 1.8 для записи.

Ответы [ 2 ]

2 голосов
/ 04 мая 2020

Я немного углубился в реализацию Open JDK и обнаружил следующее:

При вызове setRGB значения изменяются цветовой моделью изображения. В этом случае применялась следующая формула:

float red = fromsRGB8LUT16[red] & 0xffff;
float grn = fromsRGB8LUT16[grn] & 0xffff;
float blu = fromsRGB8LUT16[blu] & 0xffff;
float gray = ((0.2125f * red) +
              (0.7154f * grn) +
              (0.0721f * blu)) / 65535.0f;
intpixel[0] = (int) (gray * ((1 << nBits[0]) - 1) + 0.5f);

Это в основном пытается найти яркость данного цвета, чтобы найти его серый оттенок. Но с моими ценностями, уже являющимися серым, это должно дать тот же серый оттенок, правильно? 0.2125 + 0.7154 + 0.0721 = 1, поэтому при вводе 0xFF1E1E1E должно получиться значение серого 0xFE.

Кроме того, используемый массив fromsRGB8LUT16 не отображает значения линейно ... Вот график, который я сделал:

enter image description here

Итак, ввод 0xFF1E1E1E на самом деле значение серого 0x03! Я не совсем уверен, почему он не линейный, но он, безусловно, объясняет, почему мое выходное изображение было таким темным по сравнению с оригиналом.

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

BufferedImage grayImage = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
int[] rgbArray = buffImage.getRGB(0, 0, w, h, null, 0, w);
grayImage.getRaster().setPixels(0, 0, w, h, rgbArray);

Почему это работает? Изображение типа TYPE_BYTE_ARRAY имеет растр типа ByteInterleavedRaster, где данные хранятся в byte[], и каждое значение пикселя занимает один байт. При вызове setPixels для растра значения переданного массива просто приводятся к байту. Таким образом, 0xFF1E1E1E фактически становится 0x1E (сохраняются только младшие биты), что я и хотел.

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

2 голосов
/ 04 мая 2020

Первые две вещи, которые я протестировал, я распечатал два изображения.

BufferedImage@544fa968: type = 5 ColorModel: #pixelBits = 24 numComponents = 3 цветового пространства = java .awt. цвет. ICC_ColorSpace@68e5eea7 прозрачность = 1 имеет альфа = false isAlphaPre = false ByteInterleavedRaster: ширина = 400 высота = 400 #numDataElements 3 dataOff [0] = 2

BufferedImage@11fc564b: тип = 10 ColorModel: #pixelBits = 8 numComponents = 1 цветовое пространство = java .awt.color. ICC_ColorSpace@394a2528 прозрачность = 1 имеет альфа = false isAlphaPre = false ByteInterleavedRaster: ширина = 400 высота = 400 #numDataElements 1 dataOff [0] = 0

Мы видим, что изображения имеют другое цветовое пространство, и смещение данных отличается.

И я использовал графику для рисования исходного изображения на выходе.

Graphics g = grayImage.getGraphics();
g.drawImage(rgbImage, 0, 0, null);

Это работало нормально. Я сохранил изображение как png, но это не влияет на то, как ты видишь, и когда я взял разницу между двумя изображениями, они были одинаковыми.

Суть в том, что значения rgb различны для двух разных типов изображений. Таким образом, хотя вы видите одно и то же значение с помощью get rgb, они отображаются как разные значения при отображении.

Использование графики немного медленнее, но выдает правильное изображение.

Я думаю, что здесь различие заключается в том, что setRGB / getRGB работают с данными неинтуитивно.

DataBuffer rgbBuffer = rgbImage.getRaster().getDataBuffer();
DataBuffer grayBuffer = grayImage.getRaster().getDataBuffer();

System.out.println(grayBuffer.size() + ", " + rgbBuffer.size() );
for(int i = 0; i<10; i++){
    System.out.println(
        grayBuffer.getElem(i) + "\t"
        + rgbBuffer.getElem(3*i) + ", " 
        + rgbBuffer.getElem(3*i+1) + ", " 
        + rgbBuffer.getElem(3*i + 2) );
}

Показывает ожидаемые нами данные. Размер буфера rgb равен 3x, пиксели соответствуют напрямую.

160000, 480000
255 255, 255, 255
255 255, 255, 255
254 254, 254 , 254
253 253, 253, 253
252 252, 252, 252
252 252, 252, 252
251 251, 251, 251
251 251, 251, 251
250 250, 250, 250
250 250, 250, 250

Когда мы проверяем соответствующие значения RGB.

for(int i = 0; i<10; i++){
    System.out.println( 
        Integer.toHexString( grayImage.getRGB(i, 0) ) + ", "
        +  Integer.toHexString( rgbImage.getRGB(i, 0) ) + "  " );
}

ffffffff, ffffffff
ffffffff, ffffffff
ffffffff, fffefefe
fffefefe, fffdfdfd
fffefefe, fffcfcf c
ffffeffff *
fffdfdfd, fffbfbfb
fffdfdfd, fffbfbfb
fffdfdfd, fffafafa
fffdfdfd, fffafafa

Так, чтобы изображение было правильным, оно должно иметь разные значения rgb.

...