Я пытаюсь преобразовать изображение в градациях серого в 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
Nicolas 4 Май 2020 в 18:41

2 ответа

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

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

BufferedImage @ 11fc564b: type = 10 ColorModel: #pixelBits = 8 numComponents = 1 цветовое пространство = java.awt.color.ICC_ColorSpace@394a2528 прозрачность = 1 имеет альфа = false isAlphaPre = false ByteInterleavedRaster: width = 400 height = 400 #numDataSata [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, fffcfcfc
fffefefe, fffcfcfc
fffdfdfd, fffbfbfb
fffdfdfd, fffbfbfb
fffdfdfd, fffafafa
fffdfdfd, fffafafa

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

2
matt 4 Май 2020 в 18:14

Я немного углубился в реализацию 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
Nicolas 4 Май 2020 в 18:36