dragon66 / icafe

Java library for reading, writing, converting and manipulating images and metadata
Eclipse Public License 1.0
204 stars 58 forks source link

TIF quality issue #114

Closed ben-manes closed 1 month ago

ben-manes commented 1 year ago

I'm observing lower quality images when converting from jpeg to tif, if compared to Imagemagick. I am using 26c49d5d40 and have also tried the latest (2c0b9f557d). The identify -verbose difference between the two files shows that they are very similar in the metadata, so nothing stands out as misconfigured.

As github won't attach tif files, this zip file contains the original jpg, imagemagick's, and icafe's. images.zip

convert original.jpg -compress group4 -density 200 -units pixelsperinch -endian msb imagemagick.tif

A simplified reproducer is below. Do you have any recommendations to improve the quality?

Multi-page TIFF ```java import static com.icafe4j.image.ImageColorType.BILEVEL; import static com.icafe4j.image.tiff.TiffFieldEnum.Compression.CCITTFAX4; import static com.icafe4j.io.ByteOrder.BIG_ENDIAN; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import javax.imageio.ImageIO; import com.icafe4j.image.ImageParam; import com.icafe4j.image.options.TIFFOptions; import com.icafe4j.image.tiff.TIFFTweaker; import com.icafe4j.io.FileCacheRandomAccessOutputStream; import com.icafe4j.io.WriteStrategyMM; public class TiffTest { public static void main(String[] args) { create( Path.of("/Users/ben/Downloads/original.jpg"), Path.of("/Users/ben/Downloads/java.tif")); } public static void create(Path source, Path destination) { BufferedImage[] images = new BufferedImage[1]; try (var fileOutput = Files.newOutputStream(destination); var rout = new FileCacheRandomAccessOutputStream(fileOutput)) { images[0] = ImageIO.read(source.toFile()); TIFFOptions tiffOptions = new TIFFOptions(); tiffOptions.setTiffCompression(CCITTFAX4); tiffOptions.setByteOrder(BIG_ENDIAN); tiffOptions.setXResolution(200); tiffOptions.setYResolution(200); tiffOptions.setJPEGQuality(100); ImageParam imageSetting = ImageParam.getBuilder() .colorType(BILEVEL) .imageOptions(tiffOptions) .build(); rout.setWriteStrategy(WriteStrategyMM.getInstance()); TIFFTweaker.writeMultipageTIFF(rout, images, new ImageParam[] { imageSetting }); } catch (IOException e) { throw new UncheckedIOException(e); } finally { for (var image : images) { if (image != null) { image.getGraphics().dispose(); } } } } } ```
dragon66 commented 1 year ago

Could it be quantization and dithering? My phone couldn't open the attached zip file so haven't been able to see the actual difference but here is a link where I answered a question which may give you some hint: https://stackoverflow.com/questions/31973354/converting-pdf-to-multipage-tiff-group-4/32702373#32702373

ben-manes commented 1 year ago

Thanks, it may be. I tried dithering earlier today and Bayes was more acceptable, but not as good. I'm going to try see if that can be used as a stop gap. I converted the images back to jpeg so you can see them.

Original ![original](https://github.com/dragon66/icafe/assets/378614/c53721a3-893f-42aa-b2d2-13bc21eab694)
ImageMagick ![imagemagick](https://github.com/dragon66/icafe/assets/378614/d969d436-0923-408e-9954-76a14e558086)
iCafe ![java](https://github.com/dragon66/icafe/assets/378614/3000c833-bb04-40ab-9c8c-b8115f2966cd)
iCafe Floyd ![java_floyd](https://github.com/dragon66/icafe/assets/378614/908b7201-4393-42eb-8fa1-18fd82f27614)
iCafe Bayes ![java_bayer](https://github.com/dragon66/icafe/assets/378614/bfb9750f-4f4a-4379-97f4-d883b48afe49)
ben-manes commented 1 year ago

If comes out clear if I use a different ImageColorType like grayscale, where the compression is RLE instead of Group4. Unfortunately, my experience has been that the systems this industry are based on faxes and have very poor support for non group4 tif images. Perhaps it is something to do with IMGUtils.rgb2bilevel, but I'm not familiar with graphic algorithms enough to debug.

ben-manes commented 1 year ago

It turns out that Java 9 added support for multi-page TIFF images. I used 12Monkeys to convert to monochrome and set the dpi, though I'm sure with some digging that could be done without a library. The output metadata matches and the results are pretty good. Here's the test code if helpful and sample image (converted back to jpg for github).

12Monkeys ```java import static java.awt.AlphaComposite.Src; import static java.awt.RenderingHints.KEY_DITHERING; import static java.awt.RenderingHints.VALUE_DITHER_DISABLE; import static java.awt.image.BufferedImage.TYPE_BYTE_BINARY; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.ByteOrder; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.stream.IntStream; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import com.twelvemonkeys.image.MonochromeColorModel; import com.twelvemonkeys.imageio.metadata.tiff.Rational; import com.twelvemonkeys.imageio.metadata.tiff.TIFF; import com.twelvemonkeys.imageio.metadata.tiff.TIFFEntry; import com.twelvemonkeys.imageio.plugins.tiff.TIFFImageMetadata; public class TiffTest { public static void main(String[] args) { create( Path.of("/Users/ben/Downloads/original.jpg"), Path.of("/Users/ben/Downloads/java_x.tif")); } public static void create(Path source, Path destination) { BufferedImage[] images = new BufferedImage[1]; try (var fileOutput = Files.newOutputStream(destination)) { images[0] = convertMonochrome(ImageIO.read(source.toFile())); ImageWriter writer = ImageIO.getImageWritersByFormatName("TIFF").next(); try (var output = ImageIO.createImageOutputStream(fileOutput)) { output.setByteOrder(ByteOrder.BIG_ENDIAN); writer.setOutput(output); ImageWriteParam params = writer.getDefaultWriteParam(); params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); params.setCompressionType("CCITT T.6"); var tiffImageMetadata = new TIFFImageMetadata(List.of( new TIFFEntry(TIFF.TAG_X_RESOLUTION, new Rational(200)), new TIFFEntry(TIFF.TAG_Y_RESOLUTION, new Rational(200)))); writer.prepareWriteSequence(null); for (BufferedImage image : images) { writer.writeToSequence(new IIOImage(image, null, tiffImageMetadata), params); } writer.endWriteSequence(); } writer.dispose(); } catch (IOException e) { throw new UncheckedIOException(e); } } private static BufferedImage convertMonochrome(BufferedImage src) { if (IntStream.of(src.getSampleModel().getSampleSize()).sum() == 1) { return src; } var dst = new BufferedImage(src.getWidth(), src.getHeight(), TYPE_BYTE_BINARY, MonochromeColorModel.getInstance()); var g = dst.createGraphics(); try { g.setComposite(Src); g.setRenderingHint(KEY_DITHERING, VALUE_DITHER_DISABLE); g.drawImage(src, 0, 0, null); } finally { g.dispose(); } return dst; } } ```
Image ![12Monkeys](https://github.com/dragon66/icafe/assets/378614/6dae15c7-7613-4841-817d-61b6451852fb)
dragon66 commented 1 month ago

The threshold for the rgb2bilevel method takes the average greyscale. This can be improved by histogram analysis which I am going to try. Hope it can improve the quality.

ben-manes commented 1 month ago

Thanks @dragon66. I switched off of this library so you are welcome to close as won't do if you prefer. This was a very useful library, so thank you for all your work!

dragon66 commented 1 month ago

@ben-manes thanks for the feedback and also for bringing this issue up front. Just got some momentum from fixing some other issues and would want to give it a try anyway. Hope it can help other users.

dragon66 commented 1 month ago

Confirmed! I used a Java library from github to find the median of the original.jpg image to be 148. Plug it into my rgb2bilevel function and converted the image to BW TIFF without dithering. Voila, a crisp and sharp BW image even a bit better than the one created by ImageMagick!