2

Nearest neighbor scaling works: The entire picture stays intact when I use TYPE_NEAREST_NEIGHBOR.

Even though it is Scala code, all used libraries are standard Java libraries.

Functions:

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
}

Calling code snippet:

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

result is written to an SQLite database. If I download it from the database and save it as JPEG file, the resulting JPEG is

  • as expected if I use AffineTransformOp.TYPE_NEAREST_NEIGHBOR
  • completely black if I use AffineTransformOp.TYPE_BILINEAR
  • completely black if I use AffineTransformOp.TYPE_BICUBIC

Consequently, I accuse AffineTransformOp of being buggy... How can I solve this problem?

File magic number of result is always ff d8 ff as expected for JPEG.

Details

Java version: Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_71

Operating System: Apple, OS X 10.9.5

Test image: http://www.photos-public-domain.com/wp-content/uploads/2012/05/thundercloud-plum-blossoms.jpg

gevorg
  • 4,835
  • 4
  • 35
  • 52
ideaboxer
  • 3,863
  • 8
  • 43
  • 62
  • 1
    I tried all three combinations and all three combinations of `AffineTransformOp` and they all output something (plain Java) – MadProgrammer Jul 24 '15 at 00:44
  • 1
    Does this happen for all images, or just some? In any case, can you link a sample image for test purposes? – Harald K Jul 24 '15 at 16:33
  • Try this one or any other JPEG: http://www.photos-public-domain.com/wp-content/uploads/2012/05/thundercloud-plum-blossoms.jpg Bilinear results in completely black output. Bicubic results in completely black output. – ideaboxer Jul 31 '15 at 12:11
  • 1
    Tested it with Oracle Java 8u25 and it works. Which Java version do you use? Maybe the bug, if it was one, has been fixed already?! – Brian Jul 31 '15 at 19:43
  • 1
    Possible dup: http://stackoverflow.com/questions/9749121/java-image-rotation-with-affinetransform-outputs-black-image-but-works-well-whe – heenenee Jul 31 '15 at 19:49
  • @heenenee I did pass `null` as `dst` to `AffineTransformOp.filter(BufferedImage, BufferedImage)`, too, so I think it's not a duplicate. – Brian Jul 31 '15 at 19:56
  • @Brian it may not be reproducible on all VMs, but for where it is, the accepted answer on the other question is to create a new `BufferedImage`, so the OP should at least try that. – heenenee Jul 31 '15 at 20:00
  • I am still using Java 7. It works when I create my own new buffered image. – ideaboxer Jul 31 '15 at 20:59
  • I tried it with Java 8: It still does not work if I do not provide an image. – ideaboxer Jul 31 '15 at 23:00

1 Answers1

2

I was able to reproduce your issue on Java 1.7.0_71 on OS X 10.10.4 (I rewrote your code in Java, I can post the full code if you are interested).

In any case, the problem is not that AffineTransformOp is buggy in itself. In my test program I displayed the image using a minimal Swing JFrame and the scaled image looked all good there. This is likely why most people in the comments did not understand the problem.

Part of the issue is that the BufferedImage returned by AffineTransformOp when you don't provide a destination to the filter method (the second parameter, null in your case), it will create one for you. This image will get type BufferedImage.TYPE_INT_ARGB. Here is the relevant code from AffineTransformOp.createCompatibleDestImage() (lines 456-468, I kept the formatting, to make it easier to spot):

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);
}

Notice the special case for TYPE_NEAREST_NEIGHBOR, which explains why you'll get different behavior when using nearest neighbor algorithm. Normally this is all good, however (as I said, the image displays just fine in a Swing component).

The problem arises when you try to store this image as a JPEG. During the years, there's been a lot of confusion and issues related to the ImageIO JPEG plugin and whether it will allow you to write images with alpha channel (like your TYPE_INT_ARGB image). It does allow that. But, most often ARGB JPEGs will get misinterpreted as CMYK JPEGs (as they are 4 channels, and storing ARGB data in JPEG is very exotic) and will be displayed in all funky colors. In your case though, it seems to be all black...

So, there are two possible solutions:

  • Either write your image in a file format that supports alpha channel, like PNG or TIFF (TIFF requires an extra plugin, so it might not be the best choice). Like this:

    ImageIO.write(image, "PNG", mcios);
    
  • Or, make sure your BufferedImage is in a pixel format without alpha channel before storing as JPEG. You can do this after the scaling, but the easiest (and fastest) is to just provide the AffineTransformOp with an explicit destination image, like this:

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

Here is your image, scaled by the program, using JPEG format and the TYPE_3BYTE_BGR:

Scaled image

I'm sure you can rewrite my Java code back to Scala. :-)

Harald K
  • 26,314
  • 7
  • 65
  • 111
  • Very good explanation, thank you :-) Unfortunately, I need JPEG output. I encountered `getBounds2D` in the docs but it returns floating point values => ceil or floor? How do you know which to choose? And how do you know what `getBounds` does internally (ceil or floor)? I did not find the answer in the docs. – ideaboxer Aug 01 '15 at 10:27
  • 1
    No problem, then just choose the second option to keep JPEG format. I would generally recommend this option, but I don't know your exact requirements. :-) `getBounds()` floors x/y and ceils w/h (I looked at the source). And besides, its exactly what `AffineTransform.createCompatibleDestImage` does, so you can't go wrong with that (you don't need to take the negative x/y into account unless you also translate). – Harald K Aug 01 '15 at 10:39
  • 1
    That is true. I was able to find the source code as well and I saw the line `Rectangle r = getBounds2D(src).getBounds();`. – ideaboxer Aug 01 '15 at 10:45
  • All good then! Don't forget to upvote if you found this useful (and accept, if you think this is *the* answer)! ;-) – Harald K Aug 01 '15 at 11:05
  • 1
    I tested it successfully. Of course it is _the_ answer :-) You will receive the bounty as well in 4-5 hours. Thanks again for your great input. – ideaboxer Aug 01 '15 at 14:19