Some operations on BufferedImages with 16 bit per channel result in images with random colored pixels. Is it possible to avoid this problem?
I see the problem at least with
Sample code:
Kernel kernel = new Kernel(2, 2, new float[] { 0.25f, 0.25f, 0.25f, 0.25f });
ConvolveOp blurOp = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
img = blurOp.filter(img, null);
The operations work fine when the image is 8 bit per channel.
I tried to convert the image from 16 to 8 bit per channel while keeping the color profile using the following code but this also results in a garbled image.
private static BufferedImage changeTo8BitDepth(BufferedImage bi) {
ColorModel cm = bi.getColorModel();
boolean hasAlpha = cm.hasAlpha();
boolean isAlphaPre = cm.isAlphaPremultiplied();
int transferType = DataBuffer.TYPE_BYTE;
int transparency = cm.getTransparency();
ColorSpace cs = cm.getColorSpace();
ColorModel newCm = new ComponentColorModel(cs, hasAlpha, isAlphaPre, transparency, transferType);
WritableRaster newRaster = newCm.createCompatibleWritableRaster(bi.getWidth(), bi.getHeight());
BufferedImage newBi = new BufferedImage(newCm, newRaster, isAlphaPre, null);
// convert using setData
newBi.setData(bi.getRaster());
return newBi;
}
(It is possible to use ColorConvertOp to convert to an 8-bit sRGB image but I need the non-sRGB color profile.)
I tested on Java 8, 11, and 17 on macOS and Linux. For full source code and images for tests see https://github.com/robcast/java-imaging-test (class Test16BitColor)
After som testing and research, I think the fact that ConvolveOp
and AffineTransformOp
doesn't work with 16 bits/sample (TYPE_USHORT
data type) images out of the box, is a JDK bug. It might be that the underlying native code only works with 8 bits/sample images, but in that case "Op"s should throw an exception (or perhaps add a slower, but correct Java fallback code path). You might want to report that to the OpenJDK community.
For the 16 to 8 bits/sample conversion, the problem is you can't set 16 bit values into an 8 bit buffer, as there's no normalization done on the samples. I guess you'll just end up with the lower 8 bits of the 16 bits sample, which will typically look like static/noise. This can be fixed, however.
Here's a version that will convert the values correctly to 8 bit, but otherwise keep the color space/color profile unchanged:
private static BufferedImage changeTo8BitDepth(BufferedImage original) {
ColorModel cm = original.getColorModel();
// Create 8 bit color model
ColorModel newCM = new ComponentColorModel(cm.getColorSpace(), cm.hasAlpha(), cm.isAlphaPremultiplied(), cm.getTransparency(), DataBuffer.TYPE_BYTE);
WritableRaster newRaster = newCM.createCompatibleWritableRaster(original.getWidth(), original.getHeight());
BufferedImage newImage = new BufferedImage(newCM, newRaster, newCM.isAlphaPremultiplied(), null);
// convert using createGraphics/dawImage
Graphics2D graphics = newImage.createGraphics();
try {
graphics.drawImage(original, 0, 0, null);
}
finally {
graphics.dispose();
}
return newImage;
}
If you prefer conversion using rasters only, it's also possible with some hacks:
private static BufferedImage changeTo8BitDepth(BufferedImage original) {
ColorModel cm = original.getColorModel();
// Create 8 bit color model
ColorModel newCM = new ComponentColorModel(cm.getColorSpace(), cm.hasAlpha(), cm.isAlphaPremultiplied(), cm.getTransparency(), DataBuffer.TYPE_BYTE);
WritableRaster newRaster = newCM.createCompatibleWritableRaster(original.getWidth(), original.getHeight());
BufferedImage newImage = new BufferedImage(newCM, newRaster, newCM.isAlphaPremultiplied(), null);
// convert using setData
// newImage.setData(as8BitRaster(original.getRaster())); // Works
newRaster.setDataElements(0, 0, as8BitRaster(original.getRaster())); // Faster, requires less conversion
return newImage;
}
private static Raster as8BitRaster(WritableRaster raster) {
// Assumption: Raster is TYPE_USHORT (16 bit) and has PixelInterleavedSampleModel
PixelInterleavedSampleModel sampleModel = (PixelInterleavedSampleModel) raster.getSampleModel();
// We'll create a custom data buffer, that delegates to the original 16 bit buffer
final DataBuffer buffer = raster.getDataBuffer();
return Raster.createInterleavedRaster(new DataBuffer(DataBuffer.TYPE_BYTE, buffer.getSize()) {
@Override public int getElem(int bank, int i) {
return buffer.getElem(bank, i) >>> 8; // We only need the upper 8 bits of the 16 bit sample
}
@Override public void setElem(int bank, int i, int val) {
throw new UnsupportedOperationException("Raster is read only!");
}
}, raster.getWidth(), raster.getHeight(), sampleModel.getScanlineStride(), sampleModel.getPixelStride(), sampleModel.getBandOffsets(), new Point());
}