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`.
}
}
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;
}
Aggregations