Search in sources :

Example 16 with SampleDimension

use of org.apache.sis.coverage.SampleDimension in project sis by apache.

the class Colorizer method compact.

/**
 * Modifies the sample value ranges to make them fit in valid ranges for an {@link IndexColorModel}.
 * The {@link SampleDimension#getSampleRange()} is constrained to range [0 … 255] inclusive.
 * The {@link SampleDimension#getTransferFunction()} returns the conversion from original ranges
 * to ranges of pixel values in the colorized image.
 *
 * <p>There is two outputs: the {@link #target} sample dimension, and modifications done in-place in the
 * {@link #entries} array. For each {@link ColorsForRange} instance, the {@link ColorsForRange#sampleRange}
 * range is replaced by range of indexed colors. In addition {@code entries} elements may be reordered.</p>
 *
 * <p>If {@lini #entries} has been built from a sample dimension, that {@link SampleDimension} is specified
 * in the {@link #source} field. This is used only for providing a better name to the sample dimension.</p>
 */
private void compact() {
    if (target != null) {
        return;
    }
    /*
         * If a source SampleDimension has been specified, verify if it provides a transfer function that we can
         * use directly. If this is the case, use the existing transfer function instead of inventing our own.
         */
    ColorsForRange[] entries = this.entries;
    reuse: if (source != null) {
        target = source.forConvertedValues(false);
        if (target.getSampleRange().filter(Colorizer::isAlreadyScaled).isPresent()) {
            /*
                 * If we enter in this block, all sample values are already in the [0 … 255] range.
                 * If in addition there is no conversion to apply, then there is nothing to do.
                 */
            if (target == source) {
                return;
            }
            /*
                 * We will need to replace ranges specified in the source `SampleDimensions` by ranges used in the
                 * colorized images. Prepare in advance a `mapper` with all replacements that we know about.
                 */
            final Map<NumberRange<?>, NumberRange<?>> mapper = new HashMap<>();
            for (final Category category : target.getCategories()) {
                if (mapper.put(category.forConvertedValues(true).getSampleRange(), category.getSampleRange()) != null) {
                    // Duplicated range of values in source SampleDimensions (should not happen).
                    break reuse;
                }
            }
            /*
                 * Do the replacements in a temporary `ranges` array before to write in the `entries` array
                 * because `entries` changes must be a "all or nothing" operation. We allow each range to be
                 * used as most once.
                 */
            final NumberRange<?>[] ranges = new NumberRange<?>[entries.length];
            for (int i = 0; i < entries.length; i++) {
                if ((ranges[i] = mapper.remove(entries[i].sampleRange)) == null) {
                    // Range not found or used twice.
                    break reuse;
                }
            }
            for (int i = 0; i < entries.length; i++) {
                entries[i].sampleRange = ranges[i];
            }
            return;
        }
    }
    /*
         * If we reach this point, `source` sample dimensions were not specified or can not be used for
         * getting a transfer function to the [0 … 255] range of values. We will need to create our own.
         * First, sort the entries for having transparent colors first.
         */
    // Move transparent colors in first positions.
    Arrays.sort(entries);
    // Total span of all non-NaN ranges.
    double span = 0;
    // First available index in the [0 … 255] range.
    int lower = 0;
    // Number of entries deferred to next loop.
    int deferred = 0;
    // Total number of valid entries.
    int count = entries.length;
    // The range of values in a thematic map.
    NumberRange<?> themes = null;
    final Map<NumberRange<Integer>, ColorsForRange> mapper = new HashMap<>();
    final SampleDimension.Builder builder = new SampleDimension.Builder();
    /*
         * We will use the byte values range [0 … 255] with 0 reserved in priority for the most transparent pixels.
         * The first loop below processes NaN values, which are usually the ones associated to transparent pixels.
         * The second loop (from 0 to `deferred`) will process everything else.
         */
    for (int i = 0; i < count; i++) {
        final ColorsForRange entry = entries[i];
        NumberRange<?> sourceRange = entry.sampleRange;
        if (!entry.isData()) {
            if (lower >= MAX_VALUE) {
                throw new IllegalArgumentException(Resources.format(Resources.Keys.TooManyQualitatives));
            }
            final NumberRange<Integer> targetRange = NumberRange.create(lower, true, ++lower, false);
            if (mapper.put(targetRange, entry) == null) {
                final CharSequence name = entry.name();
                final double value = sourceRange.getMinDouble();
                /*
                     * In the usual case where we have a mix of quantitative and qualitative categories,
                     * the qualitative ones (typically "no data" categories) are associated to NaN.
                     * Values are real only if all categories are qualitatives (e.g. a thematic map).
                     * In such case we will create pseudo-quantitative categories for the purpose of
                     * computing a transfer function, but those categories should not be returned to user.
                     */
                if (Double.isNaN(value)) {
                    builder.mapQualitative(name, targetRange, (float) value);
                } else {
                    if (value == entry.sampleRange.getMaxDouble()) {
                        sourceRange = NumberRange.create(value - 0.5, true, value + 0.5, false);
                    }
                    builder.addQuantitative(name, targetRange, sourceRange);
                    themes = (themes != null) ? themes.unionAny(sourceRange) : sourceRange;
                }
            }
        } else {
            final double s = sourceRange.getSpan();
            if (s > 0) {
                // Range of real values: defer processing to next loop.
                span += s;
                System.arraycopy(entries, deferred, entries, deferred + 1, i - deferred);
                entries[deferred++] = entry;
            } else {
                // Invalid range: silently discard.
                System.arraycopy(entries, i + 1, entries, i, --count - i);
                entries[count] = null;
            }
        }
    }
    /*
         * Following block is executed only if the sample dimension defines only qualitative categories.
         * This is the case of thematic (or classification) map. It may also happen because the coverage
         * defined only a "no data" value with no information about the "real" values. In such case we
         * generate an artificial quantitative category for mapping all remaining values to [0…255] range.
         * The actual category creation happen in the loop after this block.
         */
    if (deferred == 0 && themes != null) {
        if (defaultRange == null) {
            defaultRange = NumberRange.create(0, true, Short.MAX_VALUE + 1, false);
        }
        // Following loop will usually be executed only once.
        for (final NumberRange<?> sourceRange : defaultRange.subtractAny(themes)) {
            span += sourceRange.getSpan();
            final ColorsForRange[] tmp = Arrays.copyOf(entries, ++count);
            System.arraycopy(entries, deferred, tmp, ++deferred, count - deferred);
            tmp[deferred - 1] = new ColorsForRange(null, sourceRange, new Color[] { Color.BLACK, Color.WHITE });
            entries = tmp;
        }
    }
    // Should be a no-op most of the times.
    this.entries = entries = ArraysExt.resize(entries, count);
    /*
         * Above loop mapped all NaN values. Now map the real values. Usually, there is exactly one entry taking
         * all remaining values in the [0 … 255] range, but code below is tolerant to arbitrary amount of ranges.
         */
    final int base = lower;
    final double toIndexRange = (MAX_VALUE + 1 - base) / span;
    span = 0;
    for (int i = 0; i < deferred; i++) {
        final ColorsForRange entry = entries[i];
        span += entry.sampleRange.getSpan();
        final int upper = Math.toIntExact(Math.round(span * toIndexRange) + base);
        if (upper <= lower) {
            // May happen if too many qualitative categories have been added by previous loop.
            throw new IllegalArgumentException(Resources.format(Resources.Keys.TooManyQualitatives));
        }
        final NumberRange<Integer> samples = NumberRange.create(lower, true, upper, false);
        if (mapper.put(samples, entry) == null) {
            builder.addQuantitative(entry.name(), samples, entry.sampleRange);
        }
        lower = upper;
    }
    /*
         * At this point we created a `Category` instance for each given `ColorsForRange`.
         * Update the given `ColorsForRange` instances with new range values.
         */
    if (source != null) {
        builder.setName(source.getName());
    } else {
        builder.setName(Vocabulary.format(Vocabulary.Keys.Visual));
    }
    target = builder.build();
    for (final Category category : target.getCategories()) {
        final NumberRange<?> packed = category.getSampleRange();
        mapper.get(packed).sampleRange = packed;
    // A NullPointerException on above line would be a bug in our construction of `mapper`.
    }
}
Also used : Category(org.apache.sis.coverage.Category) HashMap(java.util.HashMap) Color(java.awt.Color) SampleDimension(org.apache.sis.coverage.SampleDimension) NumberRange(org.apache.sis.measure.NumberRange) HashMap(java.util.HashMap) Map(java.util.Map)

Example 17 with SampleDimension

use of org.apache.sis.coverage.SampleDimension in project sis by apache.

the class SampleDimensions method toSampleFilters.

/**
 * Returns the {@code sampleFilters} arguments to use in a call to
 * {@link ImageProcessor#statistics ImageProcessor.statistics(…)} for excluding no-data values.
 * If the given sample dimensions are {@linkplain SampleDimension#converted() converted to units of measurement},
 * then all "no data" values are already NaN values and this method returns an array of {@code null} operators.
 * Otherwise this method returns an array of operators that covert "no data" values to {@link Double#NaN}.
 *
 * <p>This method is not in public API because it partially duplicates the work
 * of {@linkplain SampleDimension#getTransferFunction() transfer function}.</p>
 *
 * @param  processor  the processor to use for creating {@link DoubleUnaryOperator}.
 * @param  bands      the sample dimensions for which to create {@code sampleFilters}, or {@code null}.
 * @return the filters, or {@code null} if {@code bands} was null. The array may contain null elements.
 */
public static DoubleUnaryOperator[] toSampleFilters(final ImageProcessor processor, final List<SampleDimension> bands) {
    if (bands == null) {
        return null;
    }
    final DoubleUnaryOperator[] sampleFilters = new DoubleUnaryOperator[bands.size()];
    for (int i = 0; i < sampleFilters.length; i++) {
        final SampleDimension band = bands.get(i);
        if (band != null) {
            final List<Category> categories = band.getCategories();
            final Number[] nodataValues = new Number[categories.size()];
            for (int j = 0; j < nodataValues.length; j++) {
                final Category category = categories.get(j);
                if (!category.isQuantitative()) {
                    final NumberRange<?> range = category.getSampleRange();
                    final Number value;
                    if (range.isMinIncluded()) {
                        value = range.getMinValue();
                    } else if (range.isMaxIncluded()) {
                        value = range.getMaxValue();
                    } else {
                        continue;
                    }
                    nodataValues[j] = value;
                }
            }
            sampleFilters[i] = processor.filterNodataValues(nodataValues);
        }
    }
    return sampleFilters;
}
Also used : Category(org.apache.sis.coverage.Category) SampleDimension(org.apache.sis.coverage.SampleDimension) DoubleUnaryOperator(java.util.function.DoubleUnaryOperator)

Example 18 with SampleDimension

use of org.apache.sis.coverage.SampleDimension in project sis by apache.

the class SampleDimensions method backgrounds.

/**
 * Returns the background values of all bands in the given list.
 * The length of the returned array is the number of sample dimensions.
 * If a sample dimension does not declare a background value, the corresponding array element is null.
 *
 * @param  bands  the bands for which to get background values, or {@code null}.
 * @return the background values, or {@code null} if the given argument was null.
 *         Otherwise the returned array is never null but may contain null elements.
 */
public static Number[] backgrounds(final List<SampleDimension> bands) {
    if (bands == null) {
        return null;
    }
    final Number[] fillValues = new Number[bands.size()];
    for (int i = fillValues.length; --i >= 0; ) {
        final SampleDimension band = bands.get(i);
        final Optional<Number> bg = band.getBackground();
        if (bg.isPresent()) {
            fillValues[i] = bg.get();
        }
    }
    return fillValues;
}
Also used : SampleDimension(org.apache.sis.coverage.SampleDimension)

Example 19 with SampleDimension

use of org.apache.sis.coverage.SampleDimension in project sis by apache.

the class GridCoverage2D method defaultIfAbsent.

/**
 * If the sample dimensions are null, creates default sample dimensions with default names.
 * The default names are "gray", "red, green, blue" or "cyan, magenta, yellow" if the color
 * model is identified as such, or numbers if the color model is not recognized.
 *
 * @param  range     the list of sample dimensions, potentially null.
 * @param  data      the image for which to build sample dimensions, or {@code null}.
 * @param  numBands  the number of bands in the given image, or 0 if none.
 * @return the given list of sample dimensions if it was non-null, or a default list otherwise.
 */
static List<? extends SampleDimension> defaultIfAbsent(List<? extends SampleDimension> range, final RenderedImage data, final int numBands) {
    if (range == null) {
        final short[] names = (data != null) ? ImageUtilities.bandNames(data) : ArraysExt.EMPTY_SHORT;
        final SampleDimension[] sd = new SampleDimension[numBands];
        final NameFactory factory = DefaultFactories.forBuildin(NameFactory.class);
        for (int i = 0; i < numBands; i++) {
            final InternationalString name;
            final short k;
            if (i < names.length && (k = names[i]) != 0) {
                name = Vocabulary.formatInternational(k);
            } else {
                name = Vocabulary.formatInternational(Vocabulary.Keys.Band_1, i + 1);
            }
            sd[i] = new SampleDimension(factory.createLocalName(null, name), null, Collections.emptyList());
        }
        range = Arrays.asList(sd);
    }
    return range;
}
Also used : InternationalString(org.opengis.util.InternationalString) SampleDimension(org.apache.sis.coverage.SampleDimension) NameFactory(org.opengis.util.NameFactory)

Example 20 with SampleDimension

use of org.apache.sis.coverage.SampleDimension in project sis by apache.

the class ConvertedGridCoverage method converters.

/**
 * Returns the transforms for converting sample values from given sources to the {@code converted} status
 * of those sources. This method opportunistically adds the target sample dimensions in {@code target} list.
 *
 * @param  sources    {@link GridCoverage#getSampleDimensions()} of {@code source} coverage.
 * @param  targets    where to add {@link SampleDimension#forConvertedValues(boolean)} results.
 * @param  converted  {@code true} for transforms to converted values, or {@code false} for transforms to packed values.
 * @return the transforms, or {@code null} if all transforms are identity transform.
 * @throws NoninvertibleTransformException if this method can not build a full conversion chain.
 */
static MathTransform1D[] converters(final List<SampleDimension> sources, final List<SampleDimension> targets, final boolean converted) throws NoninvertibleTransformException {
    final int numBands = sources.size();
    final MathTransform1D identity = (MathTransform1D) MathTransforms.identity(1);
    final MathTransform1D[] converters = new MathTransform1D[numBands];
    Arrays.fill(converters, identity);
    for (int i = 0; i < numBands; i++) {
        final SampleDimension src = sources.get(i);
        final SampleDimension tgt = src.forConvertedValues(converted);
        targets.add(tgt);
        if (src != tgt) {
            MathTransform1D tr = src.getTransferFunction().orElse(identity);
            Optional<MathTransform1D> complete = tgt.getTransferFunction();
            if (complete.isPresent()) {
                tr = MathTransforms.concatenate(tr, complete.get().inverse());
            }
            converters[i] = tr;
        }
    }
    for (final MathTransform1D converter : converters) {
        if (!converter.isIdentity())
            return converters;
    }
    return null;
}
Also used : MathTransform1D(org.opengis.referencing.operation.MathTransform1D) SampleDimension(org.apache.sis.coverage.SampleDimension)

Aggregations

SampleDimension (org.apache.sis.coverage.SampleDimension)26 GridGeometry (org.apache.sis.coverage.grid.GridGeometry)5 Category (org.apache.sis.coverage.Category)4 GridExtent (org.apache.sis.coverage.grid.GridExtent)4 MathTransform1D (org.opengis.referencing.operation.MathTransform1D)4 RenderedImage (java.awt.image.RenderedImage)3 Test (org.junit.Test)3 IndexColorModel (java.awt.image.IndexColorModel)2 HashMap (java.util.HashMap)2 Map (java.util.Map)2 DoubleUnaryOperator (java.util.function.DoubleUnaryOperator)2 MeasurementRange (org.apache.sis.measure.MeasurementRange)2 NumberRange (org.apache.sis.measure.NumberRange)2 DataStoreContentException (org.apache.sis.storage.DataStoreContentException)2 CoordinateReferenceSystem (org.opengis.referencing.crs.CoordinateReferenceSystem)2 TransformException (org.opengis.referencing.operation.TransformException)2 Color (java.awt.Color)1 Dimension (java.awt.Dimension)1 Shape (java.awt.Shape)1 BufferedImage (java.awt.image.BufferedImage)1