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 fe65738ec3e43accdcb27df1a5f29a877c51bfd8 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sun Apr 24 17:03:49 2022 +0200 Use RGB color model when the BIL/BIP/BSQ raster has 3 or 4 bands of integer type. --- .../apache/sis/gui/coverage/BandRangeTable.java | 5 +- .../internal/coverage/j2d/ColorModelFactory.java | 57 +++++++++++++++++++++- .../sis/internal/storage/esri/RasterStore.java | 47 +++++++++++++----- 3 files changed, 94 insertions(+), 15 deletions(-) diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandRangeTable.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandRangeTable.java index 5e4111f4f1..13f7ae4fa8 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandRangeTable.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/BandRangeTable.java @@ -25,6 +25,7 @@ import javafx.scene.control.TableColumn.CellDataFeatures; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ObservableValue; import javafx.geometry.Pos; +import org.opengis.util.GenericName; import org.apache.sis.measure.NumberRange; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.util.resources.Vocabulary; @@ -38,7 +39,7 @@ import org.apache.sis.internal.gui.Styles; * the interfaces implemented by this class are implementation convenience that may change in any future version. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * @since 1.1 * @module */ @@ -138,7 +139,7 @@ final class BandRangeTable implements Callback<TableColumn<SampleDimension,Numbe final Optional<?> text; final SampleDimension sd = cell.getValue(); switch (cell.getTableColumn().getId()) { - case NAME: text = Optional.ofNullable(sd.getName()); break; + case NAME: text = Optional.ofNullable(sd.getName()).map(GenericName::toInternationalString); break; case UNITS: text = sd.getUnits(); break; default: throw new AssertionError(); // Should not happen. } diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java index c1f3093d4e..99192128ee 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/j2d/ColorModelFactory.java @@ -32,6 +32,7 @@ import java.awt.image.ComponentColorModel; import java.awt.image.SampleModel; import java.awt.image.DataBuffer; import org.apache.sis.measure.NumberRange; +import org.apache.sis.internal.util.Numerics; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.collection.WeakHashSet; @@ -481,6 +482,56 @@ public final class ColorModelFactory { return Math.abs(minimum) < 1 && Math.abs(maximum - (upper - 0.5)) < 1.5; } + /** + * Creates a color model for opaque images storing pixels using the given sample model. + * The color model can have an arbitrary number of bands, but in current implementation only one band is used. + * + * <p><b>Warning:</b> the use of this color model may be very slow and the color stretching may not be a good fit. + * This method should be used only when no standard color model can be used. This is a last resort method.</p> + * + * @param model the sample model for which to create a gray scale color model. + * @param visibleBand the band to use for computing colors. + * @param range the minimal and maximal sample value expected, or {@code null} if none. + * @return the color model for the given range of values. + */ + public static ColorModel createGrayScale(final SampleModel model, final int visibleBand, final NumberRange<?> range) { + double minimum, maximum; + if (range != null) { + minimum = range.getMinDouble(); + maximum = range.getMaxDouble(); + } else { + minimum = 0; + maximum = 1; + if (ImageUtilities.isIntegerType(model)) { + long max = Numerics.bitmask(model.getSampleSize(visibleBand)) - 1; + if (!ImageUtilities.isUnsignedType(model)) { + max >>>= 1; + minimum = ~max; // Tild operator, not minus. + } + maximum = max; + } + } + return createGrayScale(model.getDataType(), model.getNumBands(), visibleBand, minimum, maximum); + } + + /** + * Creates a RGB color model for the given sample model. + * The sample model shall use integer type and have 3 or 4 bands. + * + * @param model the sample model for which to create a color model. + * @return the color model. + */ + public static ColorModel createRGB(final SampleModel model) { + final int numBands = model.getNumBands(); + assert numBands >= 3 && numBands <= 4 : numBands; + assert ImageUtilities.isIntegerType(model) : model; + int bitsPerSample = 0; + for (int i=0; i<numBands; i++) { + bitsPerSample = Math.max(bitsPerSample, model.getSampleSize(i)); + } + return createRGB(bitsPerSample, model.getNumDataElements() == 1, numBands > 3); + } + /** * Creates a RGB color model. The {@code packed} argument should be * {@code true} for color model used with {@link java.awt.image.SinglePixelPackedSampleModel}, and @@ -540,8 +591,10 @@ public final class ColorModelFactory { * deduced. */ public static Optional<ColorModel> createSubset(final ColorModel cm, final int[] bands) { - if (cm == null) return Optional.empty(); - assert bands != null && bands.length > 0 : bands; + assert (bands != null) && bands.length > 0 : bands; + if (cm == null) { + return Optional.empty(); + } final ColorModel subset; if (cm instanceof MultiBandsIndexColorModel) { subset = ((MultiBandsIndexColorModel) cm).createSubsetColorModel(bands); diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java index 9cd4b92648..4db80b9a4b 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java @@ -45,6 +45,7 @@ import org.apache.sis.internal.coverage.j2d.ImageUtilities; import org.apache.sis.internal.storage.RangeArgument; import org.apache.sis.internal.util.UnmodifiableArrayList; import org.apache.sis.internal.util.Numerics; +import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.math.Statistics; @@ -188,11 +189,12 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource boolean computeForEachBand = false; final boolean isInteger = ImageUtilities.isIntegerType(dataType); final boolean isUnsigned = isInteger && ImageUtilities.isUnsignedType(sm); + final boolean isRGB = isInteger && (bands.length == 3 || bands.length == 4); if (stats != null && stats.count() != 0) { minimum = stats.minimum(); maximum = stats.maximum(); } else { - computeForEachBand = isInteger; + computeForEachBand = isInteger && !isRGB; } final SampleDimension.Builder builder = new SampleDimension.Builder(); for (int band=0; band < bands.length; band++) { @@ -215,26 +217,45 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource * The sample dimension is considered "converted" on the assumption that caller will replace * all "no data" value by NaN before to return the raster to the user. */ - if (name != null) { - builder.setName(name); - name = null; // Use the name only for the first band. - } - builder.addQuantitative(null, minimum, maximum, null); - if (nodataValue < minimum || nodataValue > maximum) { - builder.mapQualitative(null, nodataValue, Float.NaN); + if (isRGB) { + builder.setName(Vocabulary.formatInternational(RGB_BAND_NAMES[band])); + } else { + if (name != null) { + builder.setName(name); + name = null; // Use the name only for the first band. + } + builder.addQuantitative(null, minimum, maximum, null); + if (nodataValue < minimum || nodataValue > maximum) { + builder.mapQualitative(null, nodataValue, Float.NaN); + } } bands[band] = builder.build().forConvertedValues(!isInteger); builder.clear(); /* - * Create the color model using the statistics of the band that we choose to make visible. + * Create the color model using the statistics of the band that we choose to make visible, + * or using a RGB color model if the number of bands and the data type are compatible. */ if (band == VISIBLE_BAND) { - colorModel = ColorModelFactory.createGrayScale(dataType, sm.getNumBands(), band, minimum, maximum); + if (isRGB) { + colorModel = ColorModelFactory.createRGB(sm); + } else { + colorModel = ColorModelFactory.createGrayScale(dataType, bands.length, band, minimum, maximum); + } } } sampleDimensions = UnmodifiableArrayList.wrap(bands); } + /** + * Default names of bands when the color model is RGB or RGBA. + */ + private static final short[] RGB_BAND_NAMES = { + Vocabulary.Keys.Red, + Vocabulary.Keys.Green, + Vocabulary.Keys.Blue, + Vocabulary.Keys.Transparency + }; + /** * Creates the grid coverage resulting from a {@link #read(GridGeometry, int...)} operation. * @@ -259,7 +280,11 @@ abstract class RasterStore extends PRJDataStore implements GridCoverageResource ColorModel cm = colorModel; if (!range.isIdentity()) { bands = Arrays.asList(range.select(sampleDimensions)); - cm = range.select(colorModel).get(); + cm = range.select(colorModel).orElse(null); + if (cm == null) { + final SampleDimension band = bands.get(VISIBLE_BAND); + cm = ColorModelFactory.createGrayScale(data.getSampleModel(), VISIBLE_BAND, band.getSampleRange().orElse(null)); + } } return new GridCoverage2D(domain, bands, new BufferedImage(cm, data, false, properties)); }