Search in sources :

Example 6 with Category

use of org.apache.sis.coverage.Category 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 7 with Category

use of org.apache.sis.coverage.Category 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)

Aggregations

Category (org.apache.sis.coverage.Category)7 SampleDimension (org.apache.sis.coverage.SampleDimension)4 DoubleUnaryOperator (java.util.function.DoubleUnaryOperator)2 Color (java.awt.Color)1 Shape (java.awt.Shape)1 ColorModel (java.awt.image.ColorModel)1 IndexColorModel (java.awt.image.IndexColorModel)1 RenderedImage (java.awt.image.RenderedImage)1 SampleModel (java.awt.image.SampleModel)1 HashMap (java.util.HashMap)1 List (java.util.List)1 Locale (java.util.Locale)1 Map (java.util.Map)1 ContextMenu (javafx.scene.control.ContextMenu)1 MenuItem (javafx.scene.control.MenuItem)1 TableColumn (javafx.scene.control.TableColumn)1 TableView (javafx.scene.control.TableView)1 Statistics (org.apache.sis.math.Statistics)1 NumberRange (org.apache.sis.measure.NumberRange)1 InternationalString (org.opengis.util.InternationalString)1