Author: aaime Date: 2011-03-28 06:57:44 -0700 (Mon, 28 Mar 2011) New Revision: 36798 Added: trunk/modules/library/coverage/src/main/java/org/geotools/image/test/ trunk/modules/library/coverage/src/main/java/org/geotools/image/test/CompareImageDialog.java trunk/modules/library/coverage/src/main/java/org/geotools/image/test/ImageAssert.java trunk/modules/library/coverage/src/main/java/org/geotools/image/test/PerceptualDiff.java trunk/modules/library/coverage/src/main/java/org/geotools/image/test/ReferenceImageDialog.java trunk/modules/library/coverage/src/test/resources/org/geotools/coverage/ trunk/modules/library/coverage/src/test/resources/org/geotools/coverage/processing/ trunk/modules/library/coverage/src/test/resources/org/geotools/coverage/processing/test-data/ trunk/modules/library/coverage/src/test/resources/org/geotools/coverage/processing/test-data/readme.txt trunk/modules/library/coverage/src/test/resources/org/geotools/image/test-data/google-reproject.png Modified: trunk/modules/library/coverage/pom.xml trunk/modules/library/coverage/src/main/java/org/geotools/coverage/processing/operation/Crop.java trunk/modules/library/coverage/src/main/java/org/geotools/coverage/processing/operation/Resampler2D.java trunk/modules/library/coverage/src/test/java/org/geotools/coverage/processing/ResampleTest.java trunk/modules/library/referencing/src/main/java/org/geotools/referencing/operation/transform/WarpBuilder.java trunk/pom.xml Log: [GEOT-3484] Raster reprojection fails against areas where the projection is fully symmetrical in the area of interest Modified: trunk/modules/library/coverage/pom.xml =================================================================== --- trunk/modules/library/coverage/pom.xml 2011-03-28 13:57:00 UTC (rev 36797) +++ trunk/modules/library/coverage/pom.xml 2011-03-28 13:57:44 UTC (rev 36798) @@ -167,11 +167,18 @@ <scope>test</scope> </dependency> <dependency> + <groupId>org.geotools</groupId> + <artifactId>gt-epsg-hsql</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> <groupId>javax.media</groupId> <artifactId>jai_codec</artifactId> <!-- The version number is specified in the parent POM. --> <scope>test</scope> </dependency> + </dependencies> </project> Modified: trunk/modules/library/coverage/src/main/java/org/geotools/coverage/processing/operation/Crop.java =================================================================== --- trunk/modules/library/coverage/src/main/java/org/geotools/coverage/processing/operation/Crop.java 2011-03-28 13:57:00 UTC (rev 36797) +++ trunk/modules/library/coverage/src/main/java/org/geotools/coverage/processing/operation/Crop.java 2011-03-28 13:57:44 UTC (rev 36798) @@ -272,7 +272,9 @@ (roiParameter == null || roiParameter.getValue() ==null) ) throw new CannotCropException(Errors.format(ErrorKeys.NULL_PARAMETER_$2, PARAMNAME_ENVELOPE, GeneralEnvelope.class.toString())); - cropEnvelope = (GeneralEnvelope)envelopeParameter.getValue(); // may be null + if(envelopeParameter.getValue() != null) { + cropEnvelope = new GeneralEnvelope((Envelope) envelopeParameter.getValue()); // may be null + } // Check crop ROI try { Modified: trunk/modules/library/coverage/src/main/java/org/geotools/coverage/processing/operation/Resampler2D.java =================================================================== --- trunk/modules/library/coverage/src/main/java/org/geotools/coverage/processing/operation/Resampler2D.java 2011-03-28 13:57:00 UTC (rev 36797) +++ trunk/modules/library/coverage/src/main/java/org/geotools/coverage/processing/operation/Resampler2D.java 2011-03-28 13:57:44 UTC (rev 36798) @@ -987,7 +987,7 @@ * Otherwise we assume that the difference is caused by rounding error and we will try * progressive empirical adjustment in order to get the rectangles to fit. */ - final Warp warp = wb.buildWarp(transform, sourceBB); + final Warp warp = wb.buildWarp(transform, targetBB); if(true) { return warp; } Added: trunk/modules/library/coverage/src/main/java/org/geotools/image/test/CompareImageDialog.java =================================================================== --- trunk/modules/library/coverage/src/main/java/org/geotools/image/test/CompareImageDialog.java (rev 0) +++ trunk/modules/library/coverage/src/main/java/org/geotools/image/test/CompareImageDialog.java 2011-03-28 13:57:44 UTC (rev 36798) @@ -0,0 +1,85 @@ +package org.geotools.image.test; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.awt.image.WritableRaster; + +import javax.media.jai.widget.ScrollingImagePanel; +import javax.swing.BorderFactory; +import javax.swing.JApplet; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; + +class CompareImageDialog extends JDialog { + private static final long serialVersionUID = -8640087805737551918L; + + boolean accept = false; + + public CompareImageDialog(RenderedImage expected, RenderedImage actual, boolean showCommands) { + JPanel content = new JPanel(new BorderLayout()); + this.setContentPane(content); + this.setTitle("ImageAssert"); + final JLabel topLabel = new JLabel( + "<html><body>PerceptualDiff thinks the two images are perceptibly different.</html></body>"); + topLabel.setBorder(new EmptyBorder(4, 4, 4, 4)); + content.add(topLabel, BorderLayout.NORTH); + + JPanel central = new JPanel(new GridLayout(1, 2)); + central.add(titledImagePanel("Expected", expected)); + central.add(titledImagePanel("Actual", actual)); + content.add(central); + + JPanel commands = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton accept = new JButton("Overwrite reference"); + accept.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + CompareImageDialog.this.accept = true; + CompareImageDialog.this.setVisible(false); + } + }); + JButton reject = new JButton("Images are different"); + reject.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + CompareImageDialog.this.accept = false; + CompareImageDialog.this.setVisible(false); + } + }); + commands.add(accept); + commands.add(reject); + commands.setVisible(showCommands); + content.add(commands, BorderLayout.SOUTH); + pack(); + } + + private Component titledImagePanel(String string, RenderedImage expected) { + JPanel panel = new JPanel(new BorderLayout()); + final JLabel title = new JLabel(string); + title.setAlignmentX(0.5f); + title.setBorder(new LineBorder(Color.BLACK)); + panel.add(title, BorderLayout.NORTH); + panel.add(new ScrollingImagePanel(expected, 400, 400), BorderLayout.CENTER); + return panel; + } + + public static boolean show(RenderedImage expected, RenderedImage actual, boolean showCommands) { + CompareImageDialog dialog = new CompareImageDialog(expected, actual, showCommands); + dialog.setModal(true); + dialog.setVisible(true); + + return dialog.accept; + } + +} Added: trunk/modules/library/coverage/src/main/java/org/geotools/image/test/ImageAssert.java =================================================================== --- trunk/modules/library/coverage/src/main/java/org/geotools/image/test/ImageAssert.java (rev 0) +++ trunk/modules/library/coverage/src/main/java/org/geotools/image/test/ImageAssert.java 2011-03-28 13:57:44 UTC (rev 36798) @@ -0,0 +1,158 @@ +/* + * GeoTools - The Open Source Java GIS Toolkit + * http://geotools.org + * + * (C) 2003-2008, Open Source Geospatial Foundation (OSGeo) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +package org.geotools.image.test; + +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.awt.image.WritableRaster; +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +import javax.imageio.ImageIO; + +import org.geotools.image.ImageWorker; +import org.geotools.image.test.PerceptualDiff.Difference; +import org.geotools.util.logging.Logging; + +/** + * Compares two images using perceptual criterias: the assertions will fail if the images would look + * different to a human being + * + * @author Andrea Aime - GeoSolutions + */ +public class ImageAssert { + + static final boolean INTERACTIVE = Boolean.getBoolean("org.geotools.image.test.interactive"); + + static final Logger LOGGER = Logging.getLogger(Logger.class); + + /** + * Checks the image in the reference file and the actual image are equals from a human + * perception p.o.v + * + * @param expectedFile + * @param actualImage + * @param threshold + */ + public static void assertEquals(File expectedFile, RenderedImage actualImage, int threshold) { + assertEquals(expectedFile, actualImage, threshold, true); + } + + /** + * Checks the expected image and the actual image are equals from a human perception p.o.v + * + * @param expectedFile + * @param actualImage + * @param threshold + */ + public static void assertEquals(RenderedImage expectedImage, RenderedImage actualImage, + int threshold) { + File expectedFile = new File("target/expected.png"); + try { + ImageIO.write(expectedImage, "PNG", expectedFile); + assertEquals(expectedFile, actualImage, threshold, false); + } catch (IOException e) { + throw (Error) new AssertionError("Failed to write the image to disk").initCause(e); + } finally { + expectedFile.delete(); + } + } + + private static void assertEquals(File expectedFile, RenderedImage actualImage, int threshold, + boolean actualReferenceFile) { + // do we have the reference image at all? + if (!expectedFile.exists()) { + + // see what the user thinks of the image + boolean useAsReference = actualReferenceFile && INTERACTIVE + && ReferenceImageDialog.show(realignImage(actualImage)); + if (useAsReference) { + try { + String format = getFormat(expectedFile); + new ImageWorker(actualImage).writePNG(expectedFile, "FILTERED", 0.9f, false, false); + } catch (IOException e) { + throw (Error) new AssertionError("Failed to write the image to disk") + .initCause(e); + } + } else { + throw new AssertionError("Reference image is missing: " + expectedFile); + } + } else { + File actualFile = new File("target/actual.png"); + try { + ImageIO.write(actualImage, "PNG", actualFile); + + Difference difference = PerceptualDiff.compareImages(expectedFile, actualFile, + threshold); + if (difference.imagesDifferent) { + // check with the user + boolean overwrite = false; + if (INTERACTIVE) { + RenderedImage expectedImage = ImageIO.read(expectedFile); + overwrite = CompareImageDialog.show(realignImage(expectedImage), realignImage(actualImage), + actualReferenceFile); + } + + if (overwrite) { + ImageIO.write(actualImage, "PNG", expectedFile); + } else { + throw new AssertionError( + "Images are visibly different, PerceptualDiff output is: " + + difference.output); + } + } else { + LOGGER.info("Images are equals, PerceptualDiff output is: " + + difference.output); + + } + } catch (IOException e) { + throw (Error) new AssertionError("Failed to write the image to disk").initCause(e); + } finally { + actualFile.delete(); + } + } + } + + static String getFormat(File expectedImage) { + final String loName = expectedImage.getName().toLowerCase(); + if (loName.endsWith(".png")) { + return "PNG"; + } else if (loName.endsWith(".tif") || loName.endsWith(".tiff")) { + return "TIFF"; + } else { + throw new IllegalArgumentException("Expected image file should be a png or a tiff"); + } + } + + /** + * Makes sure the image starts at 0,0, all images coming from files do but the ones + * coming from a JAI chain might not + * @param image + * @return + */ + static final RenderedImage realignImage(RenderedImage image) { + if (image.getMinX() > 0 || image.getMinY() > 0) { + return new BufferedImage(image.getColorModel(), + ((WritableRaster) image.getData()).createWritableTranslatedChild(0, 0), image + .getColorModel().isAlphaPremultiplied(), null); + } else { + return image; + } + } + +} Added: trunk/modules/library/coverage/src/main/java/org/geotools/image/test/PerceptualDiff.java =================================================================== --- trunk/modules/library/coverage/src/main/java/org/geotools/image/test/PerceptualDiff.java (rev 0) +++ trunk/modules/library/coverage/src/main/java/org/geotools/image/test/PerceptualDiff.java 2011-03-28 13:57:44 UTC (rev 36798) @@ -0,0 +1,115 @@ +package org.geotools.image.test; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +import org.geotools.util.logging.Logging; + +/** + * Wrapper around the PerceptualDiff command line utility http://pdiff.sourceforge.net/ + * + * @author Andrea Aime - GeoSolutions + */ +class PerceptualDiff { + + static final Logger LOGGER = Logging.getLogger(PerceptualDiff.class); + + static boolean AVAILABLE; + + static class Difference { + boolean imagesDifferent; + String output; + + public Difference(boolean different, String output) { + this.imagesDifferent = different; + this.output = output; + } + }; + + static { + try { + String result = run(Arrays.asList("perceptualdiff")); + AVAILABLE = result.contains("PerceptualDiff"); + } catch (Exception e) { + + AVAILABLE = false; + } + } + + /** + * Compares two images (either png or tiffs) + * + * @param image1 + * A png/tiff file + * @param image2 + * A png/tiff file + * @param threshold + * The number of pixels to be visually different in order to consider the test a + * failure, if negative the PerceptualDiff default value will be used + * @return + */ + public static Difference compareImages(File image1, File image2, int threshold) { + if (!AVAILABLE) { + LOGGER.severe("perceptualdiff is not available, can't compare " + image1 + + " with image2"); + return new Difference(false, "Perceptual diff not available..."); + } + + try { + // run it + List<String> args = new ArrayList<String>(); + args.add("perceptualdiff"); + args.add(image1.getAbsolutePath()); + args.add(image2.getAbsolutePath()); + args.add("-fov"); + args.add("89.9"); + if (threshold > 0) { + args.add("-threshold"); + args.add(String.valueOf(threshold)); + } + String result = run(args); + + // check the results + if (result.contains("PASS")) { + return new Difference(false, result); + } else { + return new Difference(true, result); + } + } catch (Exception e) { + throw new RuntimeException("PerceptualDiff call failed!!", e); + } + } + + /** + * Runs the specified command and returns the output as a string + * + * @param cmd + * @return + * @throws IOException + * @throws InterruptedException + */ + static String run(List<String> cmd) throws IOException, InterruptedException { + // run the process and grab the output for error reporting purposes + ProcessBuilder builder = new ProcessBuilder(cmd); + StringBuilder sb = new StringBuilder(); + builder.redirectErrorStream(true); + Process p = builder.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); + String line = null; + while ((line = reader.readLine()) != null) { + if (sb != null) { + sb.append("\n"); + sb.append(line); + } + } + p.waitFor(); + + return sb.toString(); + } +} Added: trunk/modules/library/coverage/src/main/java/org/geotools/image/test/ReferenceImageDialog.java =================================================================== --- trunk/modules/library/coverage/src/main/java/org/geotools/image/test/ReferenceImageDialog.java (rev 0) +++ trunk/modules/library/coverage/src/main/java/org/geotools/image/test/ReferenceImageDialog.java 2011-03-28 13:57:44 UTC (rev 36798) @@ -0,0 +1,58 @@ +package org.geotools.image.test; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.RenderedImage; + +import javax.media.jai.widget.ScrollingImagePanel; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; + +class ReferenceImageDialog extends JDialog { + private static final long serialVersionUID = -8640087805737551918L; + + boolean accept = false; + + public ReferenceImageDialog(RenderedImage image) { + JPanel content = new JPanel(new BorderLayout()); + this.setContentPane(content); + this.setTitle("ImageAssert"); + final JLabel topLabel = new JLabel("<html><body>Reference image file is missing.<br>" + + "This is the result, do you want to make it the referecence?</html></body>"); + content.add(topLabel, BorderLayout.NORTH); + content.add(new ScrollingImagePanel(image, 400, 400)); + JPanel commands = new JPanel(new FlowLayout(FlowLayout.CENTER)); + JButton accept = new JButton("Accept as reference"); + accept.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + ReferenceImageDialog.this.accept = true; + ReferenceImageDialog.this.setVisible(false); + } + }); + JButton reject = new JButton("Reject output"); + reject.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + ReferenceImageDialog.this.accept = false; + ReferenceImageDialog.this.setVisible(false); + } + }); + commands.add(accept); + commands.add(reject); + content.add(commands, BorderLayout.SOUTH); + pack(); + } + + public static boolean show(RenderedImage ri) { + ReferenceImageDialog dialog = new ReferenceImageDialog(ri); + dialog.setModal(true); + dialog.setVisible(true); + + return dialog.accept; + } +} Modified: trunk/modules/library/coverage/src/test/java/org/geotools/coverage/processing/ResampleTest.java =================================================================== --- trunk/modules/library/coverage/src/test/java/org/geotools/coverage/processing/ResampleTest.java 2011-03-28 13:57:00 UTC (rev 36797) +++ trunk/modules/library/coverage/src/test/java/org/geotools/coverage/processing/ResampleTest.java 2011-03-28 13:57:44 UTC (rev 36798) @@ -28,9 +28,13 @@ import java.awt.geom.NoninvertibleTransformException; import java.awt.image.RenderedImage; import java.awt.image.renderable.ParameterBlock; +import java.io.File; +import javax.imageio.ImageIO; +import javax.media.jai.Interpolation; import javax.media.jai.JAI; +import org.geotools.TestData; import org.geotools.coverage.CoverageFactoryFinder; import org.geotools.coverage.grid.GeneralGridEnvelope; import org.geotools.coverage.grid.GridCoverage2D; @@ -39,6 +43,8 @@ import org.geotools.coverage.grid.ViewType; import org.geotools.coverage.processing.operation.Extrema; import org.geotools.factory.Hints; +import org.geotools.geometry.Envelope2D; +import org.geotools.image.test.ImageAssert; import org.geotools.metadata.iso.spatial.PixelTranslation; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultProjectedCRS; @@ -134,7 +140,7 @@ indexedCoverageWithTransparency = EXAMPLES.get(3); floatCoverage = EXAMPLES.get(4); ushortCoverage = EXAMPLES.get(5); - Hints.putSystemDefault(Hints.RESAMPLE_TOLERANCE, 0.0); + Hints.putSystemDefault(Hints.RESAMPLE_TOLERANCE, 0.333); } /** @@ -239,7 +245,7 @@ fail=false; Assert.assertFalse("Reprojection failed", fail); - //exception incase the target crs does not comply with the target gg crs + //exception in case the target crs does not comply with the target gg crs try{ // we supplied both crs and target gg in different crs, we get an exception backS assertEquals("Warp", showProjected(coverage,CRS.parseWKT(GOOGLE_MERCATOR_WKT), coverage.getGridGeometry(), null, true)); @@ -247,6 +253,8 @@ }catch (Exception e) { // ok! } + + } /** @@ -256,6 +264,7 @@ */ @Test public void testsNad83() throws FactoryException { + Hints.putSystemDefault(Hints.RESAMPLE_TOLERANCE, 0.0); final Hints photo = new Hints(Hints.COVERAGE_PROCESSING_VIEW, ViewType.PHOTOGRAPHIC); final CoordinateReferenceSystem crs = CRS.parseWKT( "GEOGCS[\"NAD83\"," + @@ -271,7 +280,56 @@ assertEquals("Warp", showProjected(indexedCoverageWithTransparency, crs, null, null, false)); assertEquals("Warp", showProjected(floatCoverage, crs, null, photo, true)); } + + @Test + public void testGoogleWorld() throws Exception { + File world = TestData.copy(this, "geotiff/world.tiff"); + RenderedImage image = ImageIO.read(world); + + final CoordinateReferenceSystem wgs84 = CRS.decode("EPSG:4326", true); + Envelope2D envelope = new Envelope2D(wgs84, -180, -90, 360, 180); + GridCoverage2D gcFullWorld = new GridCoverageFactory().create("world", image, envelope); + // crop, we cannot reproject it fully to the google projection + final Envelope2D cropEnvelope = new Envelope2D(wgs84, -180, -80, 360, 160); + GridCoverage2D gcCropWorld = (GridCoverage2D) Operations.DEFAULT.crop(gcFullWorld, cropEnvelope); + + // resample + Hints.putSystemDefault(Hints.RESAMPLE_TOLERANCE, 0d); + GridCoverage2D gcResampled = (GridCoverage2D) Operations.DEFAULT.resample(gcCropWorld, CRS.decode("EPSG:3857"), + null, Interpolation.getInstance(Interpolation.INTERP_BILINEAR)); + + File expected = new File("src/test/resources/org/geotools/image/test-data/google-reproject.png"); + // allow one row of difference + ImageAssert.assertEquals(expected, gcResampled.getRenderedImage(), 600); + } + + @Test + public void testWarpCompareGoogleWorld() throws Exception { + File world = TestData.copy(this, "geotiff/world.tiff"); + RenderedImage image = ImageIO.read(world); + + final CoordinateReferenceSystem wgs84 = CRS.decode("EPSG:4326", true); + Envelope2D envelope = new Envelope2D(wgs84, -180, -90, 360, 180); + GridCoverage2D gcFullWorld = new GridCoverageFactory().create("world", image, envelope); + + // crop, we cannot reproject it fully to the google projection + final Envelope2D cropEnvelope = new Envelope2D(wgs84, -180, -80, 360, 160); + GridCoverage2D gcCropWorld = (GridCoverage2D) Operations.DEFAULT.crop(gcFullWorld, cropEnvelope); + + // resample with approximation + Hints.putSystemDefault(Hints.RESAMPLE_TOLERANCE, 0.333d); + GridCoverage2D gcResampledApprox = (GridCoverage2D) Operations.DEFAULT.resample(gcCropWorld, CRS.decode("EPSG:3857"), + null, Interpolation.getInstance(Interpolation.INTERP_BILINEAR)); + + Hints.putSystemDefault(Hints.RESAMPLE_TOLERANCE, 0d); + GridCoverage2D gcResampledAccurate = (GridCoverage2D) Operations.DEFAULT.resample(gcCropWorld, CRS.decode("EPSG:3857"), + null, Interpolation.getInstance(Interpolation.INTERP_BILINEAR)); + + // allow one row of difference + ImageAssert.assertEquals(gcResampledAccurate.getRenderedImage(), gcResampledApprox.getRenderedImage(), 600); + } + /** * Tests the "Resample" operation with an "Affine" transform. */ Added: trunk/modules/library/coverage/src/test/resources/org/geotools/coverage/processing/test-data/readme.txt =================================================================== --- trunk/modules/library/coverage/src/test/resources/org/geotools/coverage/processing/test-data/readme.txt (rev 0) +++ trunk/modules/library/coverage/src/test/resources/org/geotools/coverage/processing/test-data/readme.txt 2011-03-28 13:57:44 UTC (rev 36798) @@ -0,0 +1,2 @@ +This directory must exists for proper execution of JUnit tests, even if empty. +It is used as a temporary directory for testing some image I/O operations. Added: trunk/modules/library/coverage/src/test/resources/org/geotools/image/test-data/google-reproject.png =================================================================== --- trunk/modules/library/coverage/src/test/resources/org/geotools/image/test-data/google-reproject.png (rev 0) +++ trunk/modules/library/coverage/src/test/resources/org/geotools/image/test-data/google-reproject.png 2011-03-28 13:57:44 UTC (rev 36798) @@ -0,0 +1,149 @@ +PNG + + |