This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit cddbe5f8a07f66e82d29d5af096cc8520341933d Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Mon Apr 18 19:36:09 2022 +0200 Add tests for World File image reader and fix the `GridCoverageResource` implementation. --- ide-project/NetBeans/build.xml | 2 + .../apache/sis/internal/storage/image/Image.java | 73 +++++++++++++++--- .../storage/image/SelfConsistencyTest.java | 83 +++++++++++++++++++++ .../sis/internal/storage/image/StoreTest.java | 58 ++++++++++++++ .../apache/sis/test/suite/StorageTestSuite.java | 2 + .../apache/sis/internal/storage/image/README.md | 11 +++ .../apache/sis/internal/storage/image/gradient.pgw | 6 ++ .../apache/sis/internal/storage/image/gradient.png | Bin 0 -> 176 bytes .../apache/sis/internal/storage/image/gradient.prj | 8 ++ 9 files changed, 231 insertions(+), 12 deletions(-) diff --git a/ide-project/NetBeans/build.xml b/ide-project/NetBeans/build.xml index ba9d889f9a..7e9bc0715d 100644 --- a/ide-project/NetBeans/build.xml +++ b/ide-project/NetBeans/build.xml @@ -303,6 +303,8 @@ <fileset dir="${project.root}/storage/sis-storage/src/test/resources"> <include name="**/*.txt"/> <include name="**/*.asc"/> + <include name="**/*.png"/> + <include name="**/*.pgw"/> <include name="**/*.prj"/> <include name="**/*.xml"/> </fileset> diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java index 0b6b8805e0..c803dee70b 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java @@ -21,12 +21,15 @@ import java.util.Optional; import java.io.IOException; import java.awt.Rectangle; import java.awt.image.RenderedImage; +import java.awt.image.BandedSampleModel; import javax.imageio.ImageReader; import javax.imageio.ImageReadParam; import javax.imageio.ImageTypeSpecifier; import org.opengis.util.GenericName; +import org.apache.sis.image.ImageProcessor; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridCoverage; +import org.apache.sis.coverage.grid.GridCoverage2D; import org.apache.sis.coverage.grid.GridDerivation; import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.grid.GridGeometry; @@ -36,11 +39,12 @@ import org.apache.sis.storage.DataStore; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.internal.storage.StoreResource; +import org.apache.sis.internal.storage.RangeArgument; import org.apache.sis.internal.util.UnmodifiableArrayList; +import org.apache.sis.util.ArraysExt; import org.apache.sis.util.iso.Names; import static java.lang.Math.toIntExact; -import org.apache.sis.coverage.grid.GridCoverage2D; /** @@ -172,7 +176,7 @@ class Image extends AbstractGridCoverageResource implements StoreResource { * @throws DataStoreException if an error occurred while reading the grid coverage data. */ @Override - public final GridCoverage read(GridGeometry domain, final int... range) throws DataStoreException { + public final GridCoverage read(GridGeometry domain, int... range) throws DataStoreException { synchronized (store) { final ImageReader reader = store.reader(); final ImageReadParam param = reader.getDefaultReadParam(); @@ -183,25 +187,70 @@ class Image extends AbstractGridCoverageResource implements StoreResource { final GridExtent extent = gd.getIntersection(); final int[] subsampling = gd.getSubsampling(); final int[] offsets = gd.getSubsamplingOffsets(); - domain = gd.build(); - param.setSourceSubsampling(subsampling[X_DIMENSION], subsampling[Y_DIMENSION], - offsets[X_DIMENSION], offsets[Y_DIMENSION]); - param.setSourceRegion(new Rectangle( + final int subX = subsampling[X_DIMENSION]; + final int subY = subsampling[Y_DIMENSION]; + final Rectangle region = new Rectangle( toIntExact(extent.getLow (X_DIMENSION)), toIntExact(extent.getLow (Y_DIMENSION)), toIntExact(extent.getSize(X_DIMENSION)), - toIntExact(extent.getSize(Y_DIMENSION)))); - } - if (range != null) { - param.setSourceBands(range); + toIntExact(extent.getSize(Y_DIMENSION))); + /* + * Ths subsampling offset Δx is defined differently in Image I/O and `GridGeometry`. + * The conversion from coordinate x in subsampled image to xₒ in original image is: + * + * Image I/O: xₒ = xᵣ + (x⋅s + Δx′) + * GridGeometry: xₒ = (truncate(xᵣ/s) + x)⋅s + Δx + * + * Where xᵣ is the the lower coordinate of `region`, s is the subsampling and + * `truncate(xᵣ/s)` is given by the lower coordinate of subsampled extent. + * Rearranging equations: + * + * Δx′ = truncate(xᵣ/s)⋅s + Δx - xᵣ + */ + domain = gd.build(); + GridExtent subExtent = domain.getExtent(); + param.setSourceRegion(region); + param.setSourceSubsampling(subX, subY, + toIntExact(subExtent.getLow(X_DIMENSION) * subX + offsets[X_DIMENSION] - region.x), + toIntExact(subExtent.getLow(Y_DIMENSION) * subY + offsets[Y_DIMENSION] - region.y)); } - final List<SampleDimension> sampleDimensions = getSampleDimensions(); - final RenderedImage image; + RenderedImage image; + List<SampleDimension> sampleDimensions = getSampleDimensions(); try { + /* + * If a subset of the bands is requested, ideally we should forward this request to the `ImageReader`. + * But experience suggests that not all `ImageReader` implementations support band subsetting well. + * This code applies heuristic rules forwarding the request to the image reader only for what should + * be the easiest cases. More difficult cases will be handled after the reading. + * Those heuristic rules may be changed in any future version. + */ + if (range != null) { + final ImageTypeSpecifier type = reader.getRawImageType(imageIndex); + final RangeArgument args = RangeArgument.validate(type.getNumBands(), range, listeners); + if (args.isIdentity()) { + range = null; + } else { + sampleDimensions = UnmodifiableArrayList.wrap(args.select(sampleDimensions)); + if (args.hasAllBands || type.getSampleModel() instanceof BandedSampleModel) { + range = args.getSelectedBands(); + param.setSourceBands(range); + param.setDestinationBands(ArraysExt.range(0, range.length)); + range = null; + } + } + } image = reader.readAsRenderedImage(imageIndex, param); } catch (IOException e) { throw new DataStoreException(e); } + /* + * If the reader was presumed unable to handle the band subsetting, apply it now. + * It waste some memory because unused bands still in memory. But we do that as a + * workaround for limitations in some `ImageReader` implementations. + */ + if (range != null) { + image = new ImageProcessor().selectBands(image, range); + } return new GridCoverage2D(domain, sampleDimensions, image); } } diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java new file mode 100644 index 0000000000..bf7eebff62 --- /dev/null +++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.storage.image; + +import java.net.URL; +import java.io.IOException; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.StorageConnector; +import org.apache.sis.test.storage.CoverageReadConsistency; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import static org.junit.Assume.assumeNotNull; + + +/** + * Test consistency of read operations in random domains. + * Assuming that the code reading the full extent is correct, this class can detect some bugs + * in the code reading sub-regions or applying sub-sampling. This assumption is reasonable if + * we consider that the code reading the full extent is usually simpler than the code reading + * a subset of data. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +public final strictfp class SelfConsistencyTest extends CoverageReadConsistency { + /** + * The store used for the test, opened only once. + */ + private static Store store; + + /** + * Opens the test file to be used for all tests. + * + * @throws IOException if an error occurred while opening the file. + * @throws DataStoreException if an error occurred while reading the file. + */ + @BeforeClass + public static void openFile() throws IOException, DataStoreException { + final URL url = StoreTest.class.getResource("gradient.png"); + assumeNotNull(url); + store = new Store(null, new StorageConnector(url)); + } + + /** + * Closes the test file used by all tests. + * + * @throws DataStoreException if an error occurred while closing the file. + */ + @AfterClass + public static void closeFile() throws DataStoreException { + final Store s = store; + if (s != null) { + store = null; // Clear first in case of failure. + s.close(); + } + } + + /** + * Creates a new test case. + * + * @throws DataStoreException if an error occurred while fetching the first image. + */ + public SelfConsistencyTest() throws DataStoreException { + super(store.components().iterator().next()); + } +} diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java new file mode 100644 index 0000000000..c23bfca33b --- /dev/null +++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.internal.storage.image; + +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.StorageConnector; +import org.apache.sis.storage.ProbeResult; +import org.apache.sis.test.TestCase; +import org.junit.Test; + +import static org.junit.Assert.*; + + +/** + * Tests {@link Store} and {@link StoreProvider}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +public final strictfp class StoreTest extends TestCase { + /** + * Returns a storage connector with the URL to the test data. + */ + private static StorageConnector testData() { + return new StorageConnector(StoreTest.class.getResource("gradient.png")); + } + + /** + * Tests {@link StoreProvider#probeContent(StorageConnector)} method. + * + * @throws DataStoreException if en error occurred while reading the CSV file. + */ + @Test + public void testProbeContent() throws DataStoreException { + final StoreProvider p = new StoreProvider(); + final ProbeResult r = p.probeContent(testData()); + assertTrue(r.isSupported()); + assertEquals("image/png", r.getMimeType()); + } + + +} diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java index 72f51311fe..073c8ed5f1 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java @@ -57,6 +57,8 @@ import org.junit.BeforeClass; org.apache.sis.internal.storage.wkt.StoreTest.class, org.apache.sis.internal.storage.csv.StoreProviderTest.class, org.apache.sis.internal.storage.csv.StoreTest.class, + org.apache.sis.internal.storage.image.StoreTest.class, + org.apache.sis.internal.storage.image.SelfConsistencyTest.class, org.apache.sis.internal.storage.ascii.StoreTest.class, org.apache.sis.internal.storage.ascii.WritableStoreTest.class, org.apache.sis.internal.storage.folder.StoreTest.class, diff --git a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/README.md b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/README.md new file mode 100644 index 0000000000..d009776ba6 --- /dev/null +++ b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/README.md @@ -0,0 +1,11 @@ +This repository contains an image of size 128×64 pixels georefenced +as if it was covering the world from −180° to 180° of longitude and +from −90° to 90° of latitude. Pixel values in all bands vary from 0 +inclusive to 256 exclusive with the following formulas: + + Band 0: x⋅2 + Band 1: y⋅4 + Band 2: y⋅2 + x + +It makes easy to determine the location error when the wrong pixel +is read. diff --git a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.pgw b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.pgw new file mode 100644 index 0000000000..74494b8da4 --- /dev/null +++ b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.pgw @@ -0,0 +1,6 @@ +2.8125 +0 +0 +-2.8125 +-178.59375 +88.59375 diff --git a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.png b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.png new file mode 100644 index 0000000000..e1dc33c1e7 Binary files /dev/null and b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.png differ diff --git a/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.prj b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.prj new file mode 100644 index 0000000000..74c958c5be --- /dev/null +++ b/storage/sis-storage/src/test/resources/org/apache/sis/internal/storage/image/gradient.prj @@ -0,0 +1,8 @@ +GEODCRS["WGS 84", + DATUM["World Geodetic System 1984", + ELLIPSOID["WGS 84", 6378137.0, 298.257223563, LENGTHUNIT["metre", 1]]], + CS[ellipsoidal, 2], + AXIS["Longitude (L)", east, ORDER[1]], + AXIS["Latitude (B)", north, ORDER[2]], + ANGLEUNIT["degree", 0.017453292519943295], + ID["CRS", 84, CITATION["OGC:WMS"]]]