Java AWT/ImageIO: билинейное и бикубическое масштабирование изображения JPEG приводит к полностью черному выводу

Масштабирование ближайших соседей работает: вся картинка остается неизменной, когда я использую TYPE_NEAREST_NEIGHBOR.

Несмотря на то, что это код Scala, все используемые библиотеки являются стандартными библиотеками Java.

Функции:

def getBufferedImage(imageFile: java.io.File): BufferedImage = {
    ImageIO.read(imageFile)
}

def scaleImage(image: BufferedImage, minSize: Double): BufferedImage = {
    val before: BufferedImage = image
    val w = before.getWidth()
    val h = before.getHeight()
    val affit = new AffineTransform()
    var scale = 1.0
    if(h < w) {
      if(h > 0) {
        scale = minSize / h
      }
    } else {
      if(w > 0) {
        scale = minSize / w
      }
    }
    affit.scale(scale, scale)
    val affitop = new AffineTransformOp(affit, AffineTransformOp.TYPE_BICUBIC)
    affitop.filter(before, null)
}

def getImageJpegByteArray(image: BufferedImage): Array[Byte] = {
    val baos = new java.io.ByteArrayOutputStream()
    val mcios = new MemoryCacheImageOutputStream(baos)
    ImageIO.write(image, "jpeg", mcios)
    mcios.close()
    baos.toByteArray
}

Фрагмент кода вызова:

val img = getBufferedImage(imageFile)
val scaledImg = scaleImage(img, 512)
val result = getImageJpegByteArray(scaledImg)
// result is written to SQLite database

result записывается в базу данных SQLite. Если я загружу его из базы данных и сохраню как файл JPEG, результирующий JPEG будет

  • как и ожидалось, если я использую AffineTransformOp.TYPE_NEAREST_NEIGHBOR
  • полностью черный, если я использую AffineTransformOp.TYPE_BILINEAR
  • полностью черный, если я использую AffineTransformOp.TYPE_BICUBIC

Следовательно, я обвиняю AffineTransformOp в глючности... Как я могу решить эту проблему?

Магический номер файла result всегда равен ff d8 ff, как и ожидается для JPEG.

Подробности

Версия Java: Java HotSpot(TM) 64-разрядная виртуальная машина сервера, Java 1.7.0_71

Операционная система: Apple, OS X 10.9.5

Тестовое изображение: http://www.photos-public-domain.com/wp-content/uploads/2012/05/thundercloud-plum-blossoms.jpg


person ideaboxer    schedule 23.07.2015    source источник
comment
Я попробовал все три комбинации и все три комбинации AffineTransformOp, и все они что-то выводят (обычная Java).   -  person MadProgrammer    schedule 24.07.2015
comment
Это происходит со всеми изображениями или только с некоторыми? В любом случае, можете ли вы связать образец изображения для целей тестирования?   -  person Harald K    schedule 24.07.2015
comment
Попробуйте этот или любой другой JPEG: photos-public-domain.com/wp-content/uploads/2012/05/ Билинейный результат дает полностью черный вывод. Bicubic дает полностью черный вывод.   -  person ideaboxer    schedule 31.07.2015
comment
Протестировал его с Oracle Java 8u25, и он работает. Какую версию Java вы используете? Может баг, если он был, уже исправлен?!   -  person Brian    schedule 31.07.2015
comment
Возможный дубликат: stackoverflow.com/questions/9749121/   -  person heenenee    schedule 31.07.2015
comment
@heenenee Я тоже передал null как dst в AffineTransformOp.filter(BufferedImage, BufferedImage), так что я думаю, что это не дубликат.   -  person Brian    schedule 31.07.2015
comment
@Brian, возможно, это не воспроизводится на всех виртуальных машинах, но там, где это возможно, принятым ответом на другой вопрос является создание нового BufferedImage, поэтому ОП должен хотя бы попробовать это.   -  person heenenee    schedule 31.07.2015
comment
Я все еще использую Java 7. Он работает, когда я создаю свой собственный новый буферизованный образ.   -  person ideaboxer    schedule 31.07.2015
comment
Я попробовал это с Java 8: это все равно не работает, если я не предоставлю изображение.   -  person ideaboxer    schedule 01.08.2015


Ответы (1)


Мне удалось воспроизвести вашу проблему на Java 1.7.0_71 в OS X 10.10.4 (я переписал ваш код на Java, могу опубликовать полный код, если вам интересно).

В любом случае проблема не в том, что AffineTransformOp сам по себе глючит. В моей тестовой программе я отобразил изображение, используя минимальный Swing JFrame, и масштабированное изображение там выглядело хорошо. Вероятно, поэтому большинство людей в комментариях не поняли проблемы.

Часть проблемы заключается в том, что BufferedImage, возвращаемый AffineTransformOp, когда вы не указываете место назначения для метода filter (второй параметр, null в вашем случае), он создаст его для вас. Это изображение получит тип BufferedImage.TYPE_INT_ARGB. Вот соответствующий код из AffineTransformOp.createCompatibleDestImage() (строки 456-468, я сохранил форматирование, чтобы его было легче заметить):

ColorModel cm = src.getColorModel();
if (interpolationType != TYPE_NEAREST_NEIGHBOR &&
    (cm instanceof IndexColorModel ||
     cm.getTransparency() == Transparency.OPAQUE)
{
    image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
}
else {
    image = new BufferedImage(cm,
              src.getRaster().createCompatibleWritableRaster(w,h),
              cm.isAlphaPremultiplied(), null);
}

Обратите внимание на особый случай для TYPE_NEAREST_NEIGHBOR, который объясняет, почему вы получите другое поведение при использовании алгоритма ближайшего соседа. Однако обычно все это хорошо (как я уже сказал, изображение прекрасно отображается в компоненте Swing).

Проблема возникает, когда вы пытаетесь сохранить это изображение в формате JPEG. В течение многих лет было много путаницы и проблем, связанных с плагином ImageIO JPEG и тем, позволит ли он вам записывать изображения с альфа-каналом (например, ваше изображение TYPE_INT_ARGB). Это позволяет это. Но чаще всего ARGB JPEG неправильно интерпретируются как CMYK JPEG (поскольку они 4-канальные, а хранить данные ARGB в JPEG очень экзотично) и будут отображаться во всех причудливых цветах. А в вашем случае все черное...

Итак, есть два возможных решения:

  • Либо напишите свое изображение в формате файла, который поддерживает альфа-канал, например PNG или TIFF (для TIFF требуется дополнительный плагин, поэтому это может быть не лучший выбор). Так:

    ImageIO.write(image, "PNG", mcios);
    
  • Или убедитесь, что ваш BufferedImage имеет формат пикселей без альфа-канала перед сохранением в формате JPEG. Вы можете сделать это после масштабирования, но проще всего (и быстрее всего) просто предоставить AffineTransformOp явное целевое изображение, например:

    Rectangle newSize = affitop.getBounds2D(before).getBounds();
    return affitop.filter(before, 
          new BufferedImage(newSize.width,  newSize.height, BufferedImage.TYPE_3BYTE_BGR));
    

Вот ваше изображение, масштабированное программой в формате JPEG и TYPE_3BYTE_BGR:

Масштабированное изображение

Я уверен, что вы сможете переписать мой Java-код обратно на Scala. :-)

person Harald K    schedule 01.08.2015
comment
Очень хорошее объяснение, спасибо :-) К сожалению, мне нужен вывод в формате JPEG. Я столкнулся с getBounds2D в документах, но он возвращает значения с плавающей запятой => ceil или floor? Как узнать, что выбрать? А как узнать, что getBounds делает внутри (потолок или пол)? В документах ответа не нашел. - person ideaboxer; 01.08.2015
comment
Нет проблем, тогда просто выберите второй вариант, чтобы сохранить формат JPEG. Обычно я бы рекомендовал этот вариант, но я не знаю ваших точных требований. :-) getBounds() этажей x/y и потолков w/h (я смотрел в исходниках). И, кроме того, это именно то, что делает AffineTransform.createCompatibleDestImage, так что вы не ошибетесь (вам не нужно принимать во внимание отрицательные x/y, если вы также не переводите). - person Harald K; 01.08.2015
comment
Это правда. Я также смог найти исходный код и увидел строку Rectangle r = getBounds2D(src).getBounds();. - person ideaboxer; 01.08.2015
comment
Тогда все хорошо! Не забудьте проголосовать, если вы нашли это полезным (и согласитесь, если считаете, что это ответ)! ;-) - person Harald K; 01.08.2015
comment
Я успешно протестировал его. Конечно, это ответ :-) Вы также получите награду через 4-5 часов. Еще раз спасибо за ваш большой вклад. - person ideaboxer; 01.08.2015