diff --git a/src/main/java/org/mycore/imagetiler/MCRImage.java b/src/main/java/org/mycore/imagetiler/MCRImage.java index 1385538..bf792f3 100644 --- a/src/main/java/org/mycore/imagetiler/MCRImage.java +++ b/src/main/java/org/mycore/imagetiler/MCRImage.java @@ -18,8 +18,8 @@ package org.mycore.imagetiler; import java.awt.Graphics2D; +import java.awt.Image; import java.awt.Rectangle; -import java.awt.RenderingHints; import java.awt.color.ColorSpace; import java.awt.color.ICC_ColorSpace; import java.awt.color.ICC_ProfileGray; @@ -28,7 +28,6 @@ import java.awt.image.ColorConvertOp; import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; -import java.awt.image.RescaleOp; import java.io.BufferedOutputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -125,11 +124,8 @@ public class MCRImage { private static final short TILE_SIZE_FACTOR = (short) (Math.log(TILE_SIZE) / LOG_2); - private static final double ZOOM_FACTOR = 0.5; - private static final ColorConvertOp COLOR_CONVERT_OP = new ColorConvertOp(null); - private static final ColorConvertOp SRGB_COLOR_CONVERT_OP - = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), null); + public static final float DEFAULT_GAMMA = 1F; /** @@ -187,6 +183,7 @@ public class MCRImage { } } + /** * for internal use only: uses required properties to instantiate. * @@ -397,16 +394,6 @@ private static BufferedImage convertIfNeeded(BufferedImage tile) { } final BufferedImage newTile = new BufferedImage(tile.getWidth(), tile.getHeight(), convertToGray ? BufferedImage.TYPE_BYTE_GRAY : BufferedImage.TYPE_INT_RGB); - if (colorModel.getNumColorComponents() == 1) { - //convert to sRGB first and than to grayscale to handle gamma correction - BufferedImage midStep = new BufferedImage(tile.getWidth(), tile.getHeight(), BufferedImage.TYPE_INT_RGB); - SRGB_COLOR_CONVERT_OP.filter(tile, midStep); - float gamma = getGamma(tile.getColorModel().getColorSpace()); - if (gamma != DEFAULT_GAMMA) { - gammaCorrection(gamma, midStep, newTile); - return newTile; - } - } COLOR_CONVERT_OP.filter(tile, newTile); return newTile; } @@ -419,13 +406,6 @@ private static float getGamma(ColorSpace colorSpace) { return DEFAULT_GAMMA; } - private static void gammaCorrection(float gamma, BufferedImage source, BufferedImage target) { - LOGGER.info("Correcting gamma {}.", gamma); - float gammaCorrectionFactor = (float) Math.pow(1 / gamma, -1); - RescaleOp rop = new RescaleOp(gammaCorrectionFactor, 0, null); - rop.filter(source, target); - } - /** * @return true, if gray scale image uses color map where every entry uses the same value for each color component */ @@ -450,18 +430,17 @@ private static boolean isFakeGrayScale(ColorModel colorModel) { * @param image source image * @return shrinked image */ - protected static BufferedImage scaleBufferedImage(final BufferedImage image) { + protected static BufferedImage scaleBufferedImage(final BufferedImage image, boolean useScaledInstance) { LOGGER.debug("Scaling image..."); final int width = image.getWidth(); final int height = image.getHeight(); final int newWidth = (int) Math.ceil(width / 2d); final int newHeight = (int) Math.ceil(height / 2d); + Image scaledImage = useScaledInstance ? image.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH) : null; final BufferedImage bicubic = new BufferedImage(newWidth, newHeight, getImageType(image)); - final Graphics2D bg = bicubic.createGraphics(); - bg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); - bg.scale(ZOOM_FACTOR, ZOOM_FACTOR); - bg.drawImage(image, 0, 0, null); - bg.dispose(); + Graphics2D graphics = bicubic.createGraphics(); + graphics.drawImage(useScaledInstance ? scaledImage : image, 0, 0, newWidth, newHeight, null); + graphics.dispose(); LOGGER.debug("Scaling done: {}x{}", width, height); return bicubic; } @@ -611,7 +590,7 @@ public MCRTiledPictureProps tile(MCRTileEventHandler eventHandler) throws IOExce LOGGER.debug("ImageReader: {}", imageReader.getClass()); try (ZipOutputStream zout = getZipOutputStream()) { setImageSize(imageReader); - doTile(imageReader, zout); + doTile(imageReader, zout, shouldApplyGammaCorrection(imageReader)); writeMetaData(zout); } finally { imageReader.dispose(); @@ -626,6 +605,15 @@ public MCRTiledPictureProps tile(MCRTileEventHandler eventHandler) throws IOExce return imageProperties; } + private boolean shouldApplyGammaCorrection(ImageReader imageReader) throws IOException { + ColorModel colorModel = imageReader.getRawImageType(0).getColorModel(); + if (colorModel.getNumColorComponents() == 1) { + float gamma = getGamma(colorModel.getColorSpace()); + return (gamma != DEFAULT_GAMMA); + } + return false; + } + private void setOrientation() { long start = System.nanoTime(); short orientation = 1; @@ -654,7 +642,8 @@ protected MCROrientation getOrientation() { return orientation; } - protected void doTile(final ImageReader imageReader, final ZipOutputStream zout) throws IOException { + protected void doTile(final ImageReader imageReader, final ZipOutputStream zout, boolean shouldApplyGammaCorrection) + throws IOException { BufferedImage image = getTileOfFile(imageReader, 0, 0, getImageWidth(), getImageHeight(), getOrientation(), getImageWidth(), getImageHeight()); final int zoomLevels = getZoomLevels(getImageWidth(), getImageHeight()); @@ -673,7 +662,7 @@ protected void doTile(final ImageReader imageReader, final ZipOutputStream zout) } } if (z > 0) { - image = scaleBufferedImage(image); + image = scaleBufferedImage(image, shouldApplyGammaCorrection && zoomLevels == z); } } } diff --git a/src/main/java/org/mycore/imagetiler/internal/MCRMemSaveImage.java b/src/main/java/org/mycore/imagetiler/internal/MCRMemSaveImage.java index 8fc6f77..372ff3a 100644 --- a/src/main/java/org/mycore/imagetiler/internal/MCRMemSaveImage.java +++ b/src/main/java/org/mycore/imagetiler/internal/MCRMemSaveImage.java @@ -79,7 +79,8 @@ private static void stichTiles(final BufferedImage stitchImage, final BufferedIm } @Override - protected void doTile(final ImageReader imageReader, final ZipOutputStream zout) throws IOException { + protected void doTile(final ImageReader imageReader, final ZipOutputStream zout, boolean shouldApplyGammaCorrection) + throws IOException { final int redWidth = (int) Math.ceil(getImageWidth() / ((double) megaTileSize / TILE_SIZE)); final int redHeight = (int) Math.ceil(getImageHeight() / ((double) megaTileSize / TILE_SIZE)); if (LOGGER.isDebugEnabled()) { @@ -109,16 +110,16 @@ protected void doTile(final ImageReader imageReader, final ZipOutputStream zout) LOGGER.debug("megaTile create - start tiling"); // stitch final BufferedImage tile = writeTiles(zout, megaTile, x, y, imageZoomLevels, zoomFactor, - stopOnZoomLevel); + stopOnZoomLevel, shouldApplyGammaCorrection); if (lastPhaseNeeded) { stichTiles(lastPhaseImage, tile, x * TILE_SIZE, y * TILE_SIZE); } } } if (lastPhaseNeeded) { - lastPhaseImage = scaleBufferedImage(lastPhaseImage); + lastPhaseImage = scaleBufferedImage(lastPhaseImage, false); final int lastPhaseZoomLevels = getZoomLevels(lastPhaseImage.getHeight(), lastPhaseImage.getWidth()); - writeTiles(zout, lastPhaseImage, 0, 0, lastPhaseZoomLevels, 0, 0); + writeTiles(zout, lastPhaseImage, 0, 0, lastPhaseZoomLevels, 0, 0, false); } } @@ -136,7 +137,8 @@ private void setZoomLevelPerStep(final short zoomLevel) { } private BufferedImage writeTiles(final ZipOutputStream zout, final BufferedImage megaTile, final int x, - final int y, final int imageZoomLevels, final int zoomFactor, final int stopOnZoomLevel) throws IOException { + final int y, final int imageZoomLevels, final int zoomFactor, final int stopOnZoomLevel, boolean flag) + throws IOException { final int tWidth = megaTile.getWidth(); final int tHeight = megaTile.getHeight(); BufferedImage tile = null; @@ -151,8 +153,8 @@ private BufferedImage writeTiles(final ZipOutputStream zout, final BufferedImage } } if (imageZoomLevels > stopOnZoomLevel) { - tile = scaleBufferedImage(megaTile); - return writeTiles(zout, tile, x, y, imageZoomLevels - 1, zoomFactor / 2, stopOnZoomLevel); + tile = scaleBufferedImage(megaTile, flag); + return writeTiles(zout, tile, x, y, imageZoomLevels - 1, zoomFactor / 2, stopOnZoomLevel, false); } return tile; } diff --git a/src/main/java/org/mycore/imagetiler/internal/MCRTileDebugger.java b/src/main/java/org/mycore/imagetiler/internal/MCRTileDebugger.java new file mode 100644 index 0000000..4a6450c --- /dev/null +++ b/src/main/java/org/mycore/imagetiler/internal/MCRTileDebugger.java @@ -0,0 +1,56 @@ +package org.mycore.imagetiler.internal; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.util.Optional; + +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.SwingUtilities; + +public class MCRTileDebugger { + + public static void debugImage(final String name, BufferedImage debugImage) { + if (!Optional.ofNullable(System.getProperty(MCRTileDebugger.class.getName())) + .map(Boolean::parseBoolean).orElse(false)) { + return; + } + SwingUtilities.invokeLater(() -> { + BufferedImage image = debugImage; + JFrame frame = new JFrame(name); + + // Get the size of the screen + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + + // Determine the new dimensions if the image is larger than the screen + int imageWidth = image.getWidth(); + int imageHeight = image.getHeight(); + if (imageWidth > screenSize.width || imageHeight > screenSize.height) { + double widthScale = (double) screenSize.width / imageWidth; + double heightScale = (double) screenSize.height / imageHeight; + double scale = Math.min(widthScale, heightScale); + imageWidth = (int) (imageWidth * scale); + imageHeight = (int) (imageHeight * scale); + + Image scaledImage = image.getScaledInstance(imageWidth, imageHeight, Image.SCALE_SMOOTH); + image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g2d = image.createGraphics(); + g2d.drawImage(scaledImage, 0, 0, null); + g2d.dispose(); + } + + JLabel imageLabel = new JLabel(new ImageIcon(image)); + frame.add(imageLabel); + + frame.pack(); + frame.setLocationRelativeTo(null); // Center the frame + frame.setVisible(true); + }); + + } + +} diff --git a/src/test/java/org/mycore/imagetiler/MCRImageTest.java b/src/test/java/org/mycore/imagetiler/MCRImageTest.java index 600ef1b..a075e2b 100644 --- a/src/test/java/org/mycore/imagetiler/MCRImageTest.java +++ b/src/test/java/org/mycore/imagetiler/MCRImageTest.java @@ -17,43 +17,56 @@ */ package org.mycore.imagetiler; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import java.awt.Color; import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferUShort; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; +import java.nio.channels.ByteChannel; import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystemNotFoundException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; import java.util.BitSet; +import java.util.Collections; import java.util.Comparator; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; +import java.util.stream.IntStream; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.imageio.IIOImage; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import org.apache.logging.log4j.LogManager; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.w3c.dom.Document; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - /** * Provides a good test case for {@link MCRImage}. * @author Thomas Scheffler (yagee) @@ -235,6 +248,98 @@ private static BufferedImage getStripesImage() { return stripes; } + @Test + public void testGamma() throws IOException, InterruptedException { + System.getProperties().forEach((k, v) -> System.out.println(k + "=" + v)); + Path testFilesDir = Path.of("src", "test", "resources"); + final Path file = testFilesDir.resolve("Gray Gamma 2.2.tif").toAbsolutePath(); + final MCRImage image = MCRImage.getInstance(testFilesDir, file, tileDir); + image.tile(); + try (FileSystem iviewFS = getFileSystem(tileDir.resolve("Gray Gamma 2.2.iview2"))) { + Path root = iviewFS.getRootDirectories().iterator().next(); + Path thumbnailPath = root.resolve("0/0/0.jpg"); + BufferedImage thumbnail = getBufferedImage(thumbnailPath); + int averageBrightness = getAverageBrightness(thumbnail); + BufferedImage original = getBufferedImage(file); + int averageBrightnessOrig = getAverageBrightness(original); + int averageBrightnessThumb = (int) (averageBrightness + * Math.pow(2, original.getColorModel().getPixelSize() - thumbnail.getColorModel().getPixelSize())); + //pixel count does not match between original and thumbnail + //also brightness resolution is not the same, average error should be below 2% + int marginOfError = (int) (Math.pow(2, original.getColorModel().getPixelSize()) * 0.02); + assertTrue("Brightness after gamma correction does not match.", + areNearlyEqual(averageBrightnessOrig, averageBrightnessThumb, marginOfError)); + } + + } + + public static boolean areNearlyEqual(int value1, int value2, int marginOfError) { + return Math.abs(value1 - value2) <= marginOfError; + } + + private BufferedImage getBufferedImage(Path imagePath) throws IOException { + try (ByteChannel bc = Files.newByteChannel(imagePath, StandardOpenOption.READ); + ImageInputStream imageInputStream = ImageIO.createImageInputStream(bc)) { + final Iterator readers = ImageIO.getImageReaders(imageInputStream); + if (!readers.hasNext()) { + throw new IOException("Could not read image, no image reader available"); + } + final ImageReader reader = readers.next(); + reader.setInput(imageInputStream, false, true); + try { + return reader.read(0); + } finally { + reader.dispose(); + } + } + } + + public static int getAverageBrightness(BufferedImage image) { + double totalBrightness; + + if (image.getType() == BufferedImage.TYPE_BYTE_GRAY) { + final byte[] pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + totalBrightness = IntStream.range(0, pixels.length) + .map(i -> pixels[i] & 0xFF) // Convert to unsigned + .average() + .orElse(0); + } else if (image.getType() == BufferedImage.TYPE_USHORT_GRAY) { + final short[] pixels = ((DataBufferUShort) image.getRaster().getDataBuffer()).getData(); + totalBrightness = IntStream.range(0, pixels.length) + .map(i -> pixels[i] & 0xFFFF) // Convert to unsigned + .average() + .orElse(0); + } else { + throw new IllegalArgumentException("Unsupported image type for grayscale processing"); + } + + return (int) totalBrightness; + } + + public static FileSystem getFileSystem(Path iviewFile) throws IOException { + URI uri = URI.create("jar:" + iviewFile.toUri()); + try { + return FileSystems.newFileSystem(uri, Collections.emptyMap(), MCRImageTest.class.getClassLoader()); + } catch (FileSystemAlreadyExistsException exc) { + // block until file system is closed + try { + FileSystem fileSystem = FileSystems.getFileSystem(uri); + while (fileSystem.isOpen()) { + try { + Thread.sleep(10); + } catch (InterruptedException ie) { + // get out of here + throw new IOException(ie); + } + } + } catch (FileSystemNotFoundException fsnfe) { + // seems closed now -> do nothing and try to return the file system again + LogManager.getLogger().debug("Filesystem not found", fsnfe); + } + return getFileSystem(iviewFile); + } + } + @Test public void testgetTiledFile() { String final1 = "00"; diff --git a/src/test/resources/Gray Gamma 2.2.tif b/src/test/resources/Gray Gamma 2.2.tif new file mode 100644 index 0000000..edb0b72 Binary files /dev/null and b/src/test/resources/Gray Gamma 2.2.tif differ