sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/02: Add SampleDimension.forConvertedValues(boolean) method.
Date Fri, 07 Dec 2018 19:56:37 GMT
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 54ae4c4b5178d31af06aaa0148596de740b6ac75
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Fri Dec 7 20:55:29 2018 +0100

    Add SampleDimension.forConvertedValues(boolean) method.
---
 .../java/org/apache/sis/coverage/Category.java     | 121 +++++++++------
 .../java/org/apache/sis/coverage/CategoryList.java | 112 +++++++-------
 .../org/apache/sis/coverage/ConvertedCategory.java |  90 +++++++++++
 .../org/apache/sis/coverage/SampleDimension.java   | 166 +++++++++++++++------
 .../org/apache/sis/coverage/SampleRangeFormat.java |   9 +-
 .../org/apache/sis/coverage/CategoryListTest.java  |  92 +++++++-----
 .../java/org/apache/sis/coverage/CategoryTest.java |  20 +--
 7 files changed, 406 insertions(+), 204 deletions(-)

diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
index 0e09912..d73613e 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/Category.java
@@ -126,8 +126,8 @@ public class Category implements Serializable {
      *   <li>This field is {@code null} if the minimum and maximum values are NaN (converted qualitative category).</li>
      *   <li>The value type may be different than {@link Double} (typically {@link Integer}).</li>
      *   <li>The bounds may be exclusive instead than inclusive.</li>
-     *   <li>The range may be an instance of {@link MeasurementRange} if the {@link #transferFunction}
-     *       is identity and the units of measurement are known.</li>
+     *   <li>The range may be an instance of {@link MeasurementRange} if the {@link #toConverse} is identity
+     *       and the units of measurement are known.</li>
      * </ul>
      *
      * The range is null if this category is a qualitative category converted to real values.
@@ -146,20 +146,48 @@ public class Category implements Serializable {
 
     /**
      * The conversion from sample values to real values (or conversely), never {@code null} even for qualitative
-     * categories. In the case of qualitative categories, this transfer function shall map to {@code NaN} values.
-     * In the case of sample values that are already in the units of measurement, this transfer function shall be
-     * the identity function.
+     * categories. In the case of qualitative categories, this transfer function shall map to {@code NaN} values
+     * or conversely. In the case of sample values that are already in the units of measurement, this transfer
+     * function shall be the identity function.
+     *
+     * @see #getTransferFunction()
      */
-    final MathTransform1D transferFunction;
+    final MathTransform1D toConverse;
 
     /**
-     * The category that describes sample values after {@link #transferFunction} has been applied.
+     * The category that describes values after {@linkplain #getTransferFunction() transfer function}
+     * has been applied, or if this category is already converted then the original category.
      * Never null, but may be {@code this} if the transfer function is the identity function.
+     *
+     * <p>This field establishes a bidirectional navigation between sample values and real values.
+     * This is in contrast with methods named {@code converted()}, which establish a unidirectional
+     * navigation from sample values to real values.</p>
+     *
+     * @see #converted()
+     * @see CategoryList#converse
+     * @see SampleDimension#converse
      */
-    final Category converted;
+    final Category converse;
 
     /**
-     * Constructs a qualitative or quantitative category. This constructor is provided for sub-classes.
+     * Creates a copy of the given category except for the {@link #toConverse} function which is set to identity.
+     * This is used only if a user specify a {@code ConvertedCategory} to {@link SampleDimension} constructor.
+     * Such converted category can only come from another {@code SampleDimension} and may have inconsistent
+     * information for the new sample dimension that the user is creating.
+     *
+     * @param copy  the category to copy.
+     */
+    Category(final Category copy) {
+        name       = copy.name;
+        range      = copy.range;
+        minimum    = copy.minimum;
+        maximum    = copy.maximum;
+        toConverse = identity();
+        converse   = this;
+    }
+
+    /**
+     * Constructs a qualitative or quantitative category. This constructor is accessible for sub-classing.
      * For other usages, {@link SampleDimension.Builder} should be used instead.
      *
      * @param  name       the category name (mandatory).
@@ -196,9 +224,9 @@ public class Category implements Serializable {
         try {
             final MathTransform1D toSamples;
             if (toUnits != null) {
-                transferFunction = toUnits;
+                toConverse = toUnits;
                 if (toUnits.isIdentity()) {
-                    converted = this;
+                    converse = this;
                     return;
                 }
                 toSamples = toUnits.inverse();
@@ -233,33 +261,34 @@ search:         if (!padValues.add(ordinal)) {
                  * For qualitative category, the transfer function maps to NaN while the inverse function maps back
                  * to some value in the [minimum … maximum] range. We chose the value closest to positive zero.
                  */
-                transferFunction = (MathTransform1D) MathTransforms.linear(0, MathFunctions.toNanFloat(ordinal));
+                toConverse = (MathTransform1D) MathTransforms.linear(0, MathFunctions.toNanFloat(ordinal));
                 final double value = (minimum > 0) ? minimum : (maximum <= 0) ? maximum : 0d;
                 toSamples = (MathTransform1D) MathTransforms.linear(0, value);
             }
-            converted = new Category(this, toSamples, toUnits != null, units);
+            converse = new ConvertedCategory(this, toSamples, toUnits != null, units);
         } catch (TransformException e) {
             throw new IllegalArgumentException(Resources.format(Resources.Keys.IllegalTransferFunction_1, name), e);
         }
     }
 
     /**
-     * Creates a category storing the inverse of the "sample to real values" transfer function. The {@link #transferFunction}
+     * Creates a category storing the inverse of the "sample to real values" transfer function. The {@link #toConverse}
      * of this category will convert real value in specified {@code units} to the sample (packed) value.
+     * This constructor is reserved to {@link ConvertedCategory} usage only.
      *
      * @param  original        the category storing the conversion from sample to real value.
-     * @param  toSamples       the "real to sample values" conversion, as the inverse of {@code original.transferFunction}.
+     * @param  toSamples       the "real to sample values" conversion, as the inverse of {@code original.toConverse}.
      *                         For qualitative category, this function is a constant mapping NaN to the original sample value.
      * @param  isQuantitative  {@code true} if we are construction a quantitative category, or {@code false} for qualitative.
      * @param  units           the units of measurement, or {@code null} if not applicable.
      *                         This is the source units before conversion by {@code toSamples}.
      */
-    private Category(final Category original, final MathTransform1D toSamples, final boolean isQuantitative, final Unit<?> units)
+    Category(final Category original, final MathTransform1D toSamples, final boolean isQuantitative, final Unit<?> units)
             throws TransformException
     {
-        converted        = original;
-        name             = original.name;
-        transferFunction = Objects.requireNonNull(toSamples);
+        converse   = original;
+        name       = original.name;
+        toConverse = Objects.requireNonNull(toSamples);
         /*
          * Compute 'minimum' and 'maximum' (which must be real numbers) using the conversion from samples
          * to real values. To be strict, we should use some numerical algorithm for finding a function's
@@ -277,7 +306,7 @@ search:         if (!padValues.add(ordinal)) {
                 r.getMaxDouble(),
                 r.getMinDouble(!minIncluded),
                 r.getMaxDouble(!maxIncluded)};
-        original.transferFunction.transform(extremums, 0, extremums, 0, extremums.length);
+        original.toConverse.transform(extremums, 0, extremums, 0, extremums.length);
         if (extremums[minIncluded ? 2 : 0] > extremums[maxIncluded ? 3 : 1]) {              // Compare exclusive min/max.
             ArraysExt.swap(extremums, 0, 1);                                                // Swap minimum and maximum.
             ArraysExt.swap(extremums, 2, 3);
@@ -295,15 +324,6 @@ search:         if (!padValues.add(ordinal)) {
     }
 
     /**
-     * Returns {@code false} if this instance has been created by above private constructor for real values.
-     * This method is for assertions only. We use the range type as a signature for category representing result
-     * of conversion by the transfer function.
-     */
-    final boolean isPublic() {
-        return (range != null) && !(range instanceof ConvertedRange);
-    }
-
-    /**
      * Returns the category name.
      *
      * @return the category name.
@@ -313,6 +333,15 @@ search:         if (!padValues.add(ordinal)) {
     }
 
     /**
+     * The category that describes values after {@linkplain #getTransferFunction() transfer function} has been applied.
+     * If the values are already converted (eventually to NaN values), returns {@code this}.  This method differs from
+     * {@link #converse} field in being unidirectional: navigate from sample to converted values but never backward.
+     */
+    Category converted() {
+        return converse;        // Overridden in ConvertedCategory.
+    }
+
+    /**
      * Returns {@code true} if this category is quantitative. A quantitative category has a
      * {@linkplain #getTransferFunction() transfer function} mapping sample values to values
      * in some units of measurement. By contrast, a qualitative category maps sample values
@@ -322,15 +351,8 @@ search:         if (!padValues.add(ordinal)) {
      * @return {@code true} if this category is quantitative, or
      *         {@code false} if this category is qualitative.
      */
-    public final boolean isQuantitative() {
-        /*
-         * This implementation assumes that this method will always be invoked on the instance
-         * created for sample values, never on the instance created by the private constructor.
-         * If this method was invoked on "real values category", then we would need to test for
-         * 'range' directly instead of 'converted.range'.
-         */
-        assert isPublic() : this;
-        return converted.range != null;
+    public boolean isQuantitative() {
+        return converted().range != null;
     }
 
     /**
@@ -346,7 +368,7 @@ search:         if (!padValues.add(ordinal)) {
      */
     public NumberRange<?> getSampleRange() {
         // Same assumption than in 'isQuantitative()'.
-        assert isPublic() : this;
+        assert range != null : this;
         return range;
     }
 
@@ -359,10 +381,8 @@ search:         if (!padValues.add(ordinal)) {
      * @see SampleDimension#getMeasurementRange()
      */
     public Optional<MeasurementRange<?>> getMeasurementRange() {
-        // Same assumption than in 'isQuantitative()'.
-        assert isPublic() : this;
         // A ClassCastException below would be a bug in our constructor.
-        return Optional.ofNullable((MeasurementRange<?>) converted.range);
+        return Optional.ofNullable((MeasurementRange<?>) converted().range);
     }
 
     /**
@@ -393,10 +413,17 @@ search:         if (!padValues.add(ordinal)) {
          * This implementation assumes that this method will always be invoked on the instance
          * created for sample values, never on the instance created by the private constructor.
          * If this method was invoked on "real values category", then we would need to return
-         * the identity transform instead than 'transferFunction'.
+         * the identity transform instead than 'toConverse'. This is done by ConvertedCategory.
          */
-//      assert isPublic();     — invoked by isQuantitative().
-        return isQuantitative() ? Optional.of(transferFunction) : Optional.empty();
+        assert range != null : this;
+        return (converse.range != null) ? Optional.of(toConverse) : Optional.empty();
+    }
+
+    /**
+     * Returns the identity transform. This is the value returned by {@link ConvertedCategory#getTransferFunction()}.
+     */
+    static MathTransform1D identity() {
+        return (MathTransform1D) MathTransforms.identity(1);
     }
 
     /**
@@ -420,12 +447,12 @@ search:         if (!padValues.add(ordinal)) {
             // Slight optimization
             return true;
         }
-        if (object instanceof Category) {
+        if (object != null && getClass().equals(object.getClass())) {
             final Category that = (Category) object;
             return name.equals(that.name) && Objects.equals(range, that.range) &&
                    Double.doubleToRawLongBits(minimum) == Double.doubleToRawLongBits(that.minimum) &&
                    Double.doubleToRawLongBits(maximum) == Double.doubleToRawLongBits(that.maximum) &&
-                   transferFunction.equals(that.transferFunction);
+                   toConverse.equals(that.toConverse);
         }
         return false;
     }
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
index 944341c..5e51fa2 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
@@ -61,11 +61,6 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
     static final CategoryList EMPTY = new CategoryList();
 
     /**
-     * The result of converting sample values to real values, never {@code null}.
-     */
-    final CategoryList converted;
-
-    /**
      * The union of the ranges of every categories, excluding {@code NaN} values.
      * May be {@code null} if this list has no non-{@code NaN} category.
      *
@@ -115,15 +110,29 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
     private transient Category last;
 
     /**
+     * The {@code CategoryList} that describes values after {@linkplain #getTransferFunction() transfer function}
+     * has been applied, or if this {@code CategoryList} is already converted then the original {@code CategoryList}.
+     * Never null, but may be {@code this} if the transfer function is the identity function.
+     *
+     * <p>This field establishes a bidirectional navigation between sample values and real values.
+     * This is in contrast with methods named {@code converted()}, which establish a unidirectional
+     * navigation from sample values to real values.</p>
+     *
+     * @see Category#converse
+     * @see SampleDimension#converse
+     */
+    final CategoryList converse;
+
+    /**
      * The constructor for the {@link #EMPTY} constant.
      */
     private CategoryList() {
-        converted     = this;
         range         = null;
         minimums      = ArraysExt.EMPTY_DOUBLE;
         categories    = new Category[0];
         main          = null;
         extrapolation = null;
+        converse      = this;
     }
 
     /**
@@ -132,11 +141,25 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
      *
      * @param  categories  the list of categories. May be empty, but can not be null.
      *                     This array is not cloned and is modified in-place.
-     * @param  inverse     if we are creating the list of categories after conversion from samples to real values,
+     * @param  converse    if we are creating the list of categories after conversion from samples to real values,
      *                     the original list before conversion. Otherwise {@code null}.
      * @throws IllegalArgumentException if two or more categories have overlapping sample value range.
      */
-    CategoryList(final Category[] categories, CategoryList inverse) {
+    CategoryList(final Category[] categories, CategoryList converse) {
+        /*
+         * If users specify Category instances themselves, maybe they took existing instances from another
+         * sample dimension. A list of "non-converted" categories should not contain any ConvertedCategory
+         * instances, otherwise confusion will occur later.  Note that the converse is not true: a list of
+         * converted categories may contain plain Category instances if the conversion is identity.
+         */
+        if (converse == null) {
+            for (int i=0; i<categories.length; i++) {
+                final Category c = categories[i];
+                if (c instanceof ConvertedCategory) {
+                    categories[i] = new Category(c);
+                }
+            }
+        }
         Arrays.sort(categories, Category.COMPARATOR);
         this.categories = categories;
         /*
@@ -163,7 +186,7 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
             }
             final double minimum = category.minimum;
             minimums[i] = minimum;
-            if (category.converted.range != null) {                         // Category.isQuantitative() without assert.
+            if (category.converse.range != null) {                          // Category.isQuantitative() inlined.
                 final double span = category.maximum - minimum;             // NaN if "converted qualitative" category.
                 if (span >= widest) {
                     widest = span;
@@ -194,15 +217,15 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
          *     list. This is the last category to have a range of real (non-NaN) numbers.
          */
         Category extrapolation = null;
-        if (inverse == null) {
+        if (converse == null) {
             boolean hasConversion = false;
             final Category[] convertedCategories = new Category[categories.length];
             for (int i=0; i < convertedCategories.length; i++) {
                 final Category category = categories[i];
-                hasConversion |= (category != category.converted);
-                convertedCategories[i] = category.converted;
+                hasConversion |= (category != category.converse);
+                convertedCategories[i] = category.converse;
             }
-            inverse = hasConversion ? new CategoryList(convertedCategories, this) : this;
+            converse = hasConversion ? new CategoryList(convertedCategories, this) : this;
         } else {
             for (int i=categories.length; --i >= 0;) {
                 final Category category = categories[i];
@@ -213,7 +236,7 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
             }
         }
         this.extrapolation = extrapolation;
-        converted = inverse;
+        this.converse      = converse;
     }
 
     /**
@@ -232,40 +255,19 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
     }
 
     /**
-     * Returns {@code false} if this instance contains private categories.
-     * This method is for assertions only.
-     */
-    final boolean isPublic() {
-        for (final Category c : categories) {
-            if (!c.isPublic()) return false;
-        }
-        return true;
-    }
-
-    /**
-     * Returns {@code true} if this list contains at least one quantitative category.
-     * We use the converted range has a criterion, since it shall be null if the result
-     * of all conversions is NaN.
-     *
-     * @see Category#isQuantitative()
-     */
-    final boolean hasQuantitative() {
-        assert isPublic();
-        return converted.range != null;
-    }
-
-    /**
      * Returns the <cite>transfer function</cite> from sample values to real values, including conversion
-     * of "no data" value to NaN. If there is no quantitative categories, returns {@code null}.
+     * of "no data" value to NaN.  Callers should ensure that there is at least one quantitative category
+     * before to invoke this method.
      *
      * @see SampleDimension#getTransferFunction()
      */
     final MathTransform1D getTransferFunction() {
         MathTransform1D tr = null;
-        if (hasQuantitative()) {
-            tr = categories[0].transferFunction;
-            for (int i=1; i<categories.length; i++) {
-                if (!tr.equals(categories[i].transferFunction)) {
+        final int n = categories.length;
+        if (n != 0) {
+            tr = categories[0].toConverse;
+            for (int i=1; i<n; i++) {
+                if (!tr.equals(categories[i].toConverse)) {
                     tr = this;
                     break;
                 }
@@ -454,7 +456,7 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
                 srcOff -= count - 1;
             }
             final int stepOff = srcOff + srcToDst;
-            final MathTransform1D piece = category.transferFunction;
+            final MathTransform1D piece = category.toConverse;
             if (srcFloat != null) {
                 if (dstFloat != null) {
                     piece.transform(srcFloat, srcOff, dstFloat, stepOff, count);
@@ -470,10 +472,10 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
             }
             if (extrapolation != null) {
                 dstOff = srcOff + srcToDst;
-                final Category cnv = category.converted;
+                final Category converse = category.converse;
                 if (dstFloat != null) {                                 // Loop for the 'float' version.
-                    final float min = (float) cnv.minimum;
-                    final float max = (float) cnv.maximum;
+                    final float min = (float) converse.minimum;
+                    final float max = (float) converse.maximum;
                     while (--count >= 0) {
                         final float check = dstFloat[dstOff];
                         if (check < min) {
@@ -484,8 +486,8 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
                         dstOff++;
                     }
                 } else {                                                // Loop for the 'double' version.
-                    final double min = cnv.minimum;
-                    final double max = cnv.maximum;
+                    final double min = converse.minimum;
+                    final double max = converse.maximum;
                     while (--count >= 0) {
                         final double check = dstPts[dstOff];
                         if (check < min) {
@@ -563,13 +565,13 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
             }
             last = category;
         }
-        value = category.transferFunction.transform(value);
+        value = category.toConverse.transform(value);
         if (extrapolation != null) {
             double bound;
-            if (value < (bound = category.converted.minimum)) return bound;
-            if (value > (bound = category.converted.maximum)) return bound;
+            if (value < (bound = category.converse.minimum)) return bound;
+            if (value > (bound = category.converse.maximum)) return bound;
         }
-        assert category == converted.search(value).converted : category;
+        assert category == converse.search(value).converse : category;
         return value;
     }
 
@@ -592,7 +594,7 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
             }
             last = category;
         }
-        return category.transferFunction.derivative(value);
+        return category.toConverse.derivative(value);
     }
 
     /**
@@ -626,7 +628,7 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
      */
     @Override
     public boolean isIdentity() {
-        return converted == this;
+        return converse == this;
     }
 
     /**
@@ -635,7 +637,7 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public final MathTransform1D inverse() {
-        return converted;
+        return converse;
     }
 
     /**
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedCategory.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedCategory.java
new file mode 100644
index 0000000..f73cec7
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/ConvertedCategory.java
@@ -0,0 +1,90 @@
+/*
+ * 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.coverage;
+
+import java.util.Optional;
+import javax.measure.Unit;
+import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.measure.NumberRange;
+
+
+/**
+ * A category of "real values" range. Sample values in this category are equal to real values.
+ * By definition, the {@link #getTransferFunction()} method for this class returns the identity transform,
+ * or an empty optional if this category is a qualitative one.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class ConvertedCategory extends Category {
+    /**
+     * Serial number for inter-operability with different versions.
+     */
+    private static final long serialVersionUID = -7164422654831370784L;
+
+    /**
+     * Creates a category storing the inverse of the "sample to real values" transfer function. The {@link #toConverse}
+     * of this category will convert real value in specified {@code units} to the sample (packed) value.
+     *
+     * @param  original        the category storing the conversion from sample to real value.
+     * @param  toSamples       the "real to sample values" conversion, as the inverse of {@code original.transferFunction}.
+     *                         For qualitative category, this function is a constant mapping NaN to the original sample value.
+     * @param  isQuantitative  {@code true} if we are construction a quantitative category, or {@code false} for qualitative.
+     * @param  units           the units of measurement, or {@code null} if not applicable.
+     *                         This is the source units before conversion by {@code toSamples}.
+     */
+    ConvertedCategory(final Category original, final MathTransform1D toSamples, final boolean isQuantitative, final Unit<?> units)
+            throws TransformException
+    {
+        super(original, toSamples, isQuantitative, units);
+    }
+
+    /**
+     * Returns {@code this} since the values represented by {@code ConvertedCategory} are already converted.
+     */
+    @Override
+    Category converted() {
+        return this;
+    }
+
+    /**
+     * Returns the range of value, which is the same as {@link #getMeasurementRange()} unless the values are NaN.
+     */
+    @Override
+    public NumberRange<?> getSampleRange() {
+        if (range != null) {
+            return range;
+        }
+        Float min = (float) minimum;        // Should be NaN produced by MathFunctions.toNanFloat(int).
+        Float max = (float) maximum;
+        if (max.equals(min)) max = min;
+        // Do not use NumberRange.create(float, …) because it rejects NaN values.
+        return new NumberRange<>(Float.class, min, true, max, true);
+    }
+
+    /**
+     * Returns the <cite>transfer function</cite> from sample values to real values in units of measurement.
+     * The function is absent if this category is not a {@linkplain #isQuantitative() quantitative} category.
+     */
+    @Override
+    public Optional<MathTransform1D> getTransferFunction() {
+        return (range != null) ? Optional.of(identity()) : Optional.empty();
+    }
+}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
index 7682122..e670901 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@ -25,8 +25,6 @@ import java.util.Optional;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Locale;
-import java.io.IOException;
-import java.io.ObjectInputStream;
 import java.io.Serializable;
 import javax.measure.Unit;
 import org.opengis.util.InternationalString;
@@ -99,7 +97,38 @@ public class SampleDimension implements Serializable {
      *
      * @see #getTransferFunction()
      */
-    private transient MathTransform1D transferFunction;
+    private final MathTransform1D transferFunction;
+
+    /**
+     * The {@code SampleDimension} that describes values after {@linkplain #getTransferFunction() transfer function}
+     * has been applied, or if this {@code SampleDimension} is already converted then the original sample dimension.
+     * May be {@code null} if this sample dimension has no transfer function, or {@code this} if the transfer function
+     * is the identity function.
+     *
+     * <p>This field establishes a bidirectional navigation between sample values and real values.
+     * This is in contrast with methods named {@link #converted()}, which establish a unidirectional
+     * navigation from sample values to real values.</p>
+     *
+     * @see #converted()
+     * @see Category#converse
+     * @see CategoryList#converse
+     */
+    private final SampleDimension converse;
+
+    /**
+     * Creates a new sample dimension for values that are already converted to real values.
+     * This transfer function is set to identity, which implies that this constructor should
+     * be invoked only for sample dimensions having at least one quantitative category.
+     *
+     * @param  original  the original sample dimension for packed values.
+     */
+    private SampleDimension(final SampleDimension original) {
+        converse         = original;
+        name             = original.name;
+        categories       = original.categories.converse;
+        transferFunction = Category.identity();
+        assert hasQuantitative();
+    }
 
     /**
      * Creates a sample dimension with the specified name and categories.
@@ -123,21 +152,29 @@ public class SampleDimension implements Serializable {
                 name = Vocabulary.formatInternational(Vocabulary.Keys.Untitled);
             }
         }
-        this.name        = Types.toInternationalString(name);
-        this.categories  = list;
-        transferFunction = list.getTransferFunction();
+        this.name       = Types.toInternationalString(name);
+        this.categories = list;
+        if (list.range == null) {               // !hasQuantitative() inlined since we can not yet invoke that method.
+            transferFunction = null;
+            converse = this;
+        } else if (list == list.converse) {
+            transferFunction = Category.identity();
+            converse = this;
+        } else {
+            transferFunction = list.getTransferFunction();
+            converse = new SampleDimension(this);
+        }
     }
 
     /**
-     * Computes transient fields after deserialization.
+     * Returns the sample dimension that describes real values. This method establishes a unidirectional navigation
+     * from sample values to real values. This is in contrast to {@link #converse}, which establish a bidirectional
+     * navigation.
      *
-     * @param  in  the input stream from which to deserialize a sample dimension.
-     * @throws IOException if an I/O error occurred while reading or if the stream contains invalid data.
-     * @throws ClassNotFoundException if the class serialized on the stream is not on the classpath.
+     * @see #forConvertedValues(boolean)
      */
-    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
-        in.defaultReadObject();
-        transferFunction = categories.getTransferFunction();
+    private SampleDimension converted() {
+        return (converse != null && transferFunction != null && !transferFunction.isIdentity()) ? converse : this;
     }
 
     /**
@@ -164,6 +201,17 @@ public class SampleDimension implements Serializable {
     }
 
     /**
+     * Returns {@code true} if this list contains at least one quantitative category.
+     * We use the converted range has a criterion, since it shall be null if the result
+     * of all conversions is NaN.
+     *
+     * @see Category#isQuantitative()
+     */
+    private boolean hasQuantitative() {
+        return converted().categories.range != null;
+    }
+
+    /**
      * Returns the values to indicate "no data" for this sample dimension.
      *
      * @return the values to indicate no data values for this sample dimension, or an empty set if none.
@@ -171,39 +219,42 @@ public class SampleDimension implements Serializable {
      *         because some ranges contain an infinite amount of values.
      */
     public Set<Number> getNoDataValues() {
-        if (!categories.hasQuantitative()) {
-            return Collections.emptySet();
-        }
-        final NumberRange<?>[] ranges = new NumberRange<?>[categories.size()];
-        Class<? extends Number> widestClass = Byte.class;
-        int count = 0;
-        for (final Category c : categories) {
-            if (!c.isQuantitative()) {
-                if (!c.range.isBounded()) {
-                    throw new IllegalStateException(Resources.format(Resources.Keys.CanNotEnumerateValuesInRange_1, c.range));
+        if (hasQuantitative()) {
+            final NumberRange<?>[] ranges = new NumberRange<?>[categories.size()];
+            Class<? extends Number> widestClass = Byte.class;
+            int count = 0;
+            for (final Category category : categories) {
+                final NumberRange<?> range = category.range;
+                if (range != null && !category.isQuantitative()) {
+                    if (!range.isBounded()) {
+                        throw new IllegalStateException(Resources.format(Resources.Keys.CanNotEnumerateValuesInRange_1, range));
+                    }
+                    widestClass = Numbers.widestClass(widestClass, range.getElementType());
+                    ranges[count++] = range;
                 }
-                widestClass = Numbers.widestClass(widestClass, c.range.getElementType());
-                ranges[count++] = c.range;
             }
-        }
-        final Set<Number> noDataValues = new TreeSet<>();
-        for (int i=0; i<count; i++) {
-            final NumberRange<?> range = ranges[i];
-            final Number minimum = range.getMinValue();
-            final Number maximum = range.getMaxValue();
-            if (range.isMinIncluded()) noDataValues.add(Numbers.cast(minimum, widestClass));
-            if (range.isMaxIncluded()) noDataValues.add(Numbers.cast(maximum, widestClass));
-            if (Numbers.isInteger(range.getElementType())) {
-                long value = minimum.longValue() + 1;       // If value was inclusive, then it has already been added to the set.
-                long stop  = maximum.longValue() - 1;
-                while (value <= stop) {
-                    noDataValues.add(Numbers.wrap(value, widestClass));
+            if (count != 0) {
+                final Set<Number> noDataValues = new TreeSet<>();
+                for (int i=0; i<count; i++) {
+                    final NumberRange<?> range = ranges[i];
+                    final Number minimum = range.getMinValue();
+                    final Number maximum = range.getMaxValue();
+                    if (range.isMinIncluded()) noDataValues.add(Numbers.cast(minimum, widestClass));
+                    if (range.isMaxIncluded()) noDataValues.add(Numbers.cast(maximum, widestClass));
+                    if (Numbers.isInteger(range.getElementType())) {
+                        long value = minimum.longValue() + 1;       // If value was inclusive, then it has already been added to the set.
+                        long stop  = maximum.longValue() - 1;
+                        while (value <= stop) {
+                            noDataValues.add(Numbers.wrap(value, widestClass));
+                        }
+                    } else if (!minimum.equals(maximum)) {
+                        throw new IllegalStateException(Resources.format(Resources.Keys.CanNotEnumerateValuesInRange_1, range));
+                    }
                 }
-            } else if (!minimum.equals(maximum)) {
-                throw new IllegalStateException(Resources.format(Resources.Keys.CanNotEnumerateValuesInRange_1, range));
+                return noDataValues;
             }
         }
-        return noDataValues;
+        return Collections.emptySet();
     }
 
     /**
@@ -229,7 +280,7 @@ public class SampleDimension implements Serializable {
      */
     public Optional<MeasurementRange<?>> getMeasurementRange() {
         // A ClassCastException below would be a bug in our constructors.
-        return Optional.ofNullable((MeasurementRange<?>) categories.converted.range);
+        return Optional.ofNullable((MeasurementRange<?>) converted().categories.range);
     }
 
     /**
@@ -261,10 +312,11 @@ public class SampleDimension implements Serializable {
     public Optional<TransferFunction> getTransferFunctionFormula() {
         MathTransform1D tr = null;
         for (final Category category : categories) {
-            if (category.isQuantitative()) {
+            final Optional<MathTransform1D> c = category.getTransferFunction();
+            if (c.isPresent()) {
                 if (tr == null) {
-                    tr = category.transferFunction;
-                } else if (!tr.equals(category.transferFunction)) {
+                    tr = c.get();
+                } else if (!tr.equals(c.get())) {
                     throw new IllegalStateException(Resources.format(Resources.Keys.CanNotSimplifyTransferFunction_1));
                 }
             }
@@ -293,7 +345,8 @@ public class SampleDimension implements Serializable {
      */
     public Optional<Unit<?>> getUnits() {
         Unit<?> main = null;
-        for (final Category c : categories.converted) {
+        final SampleDimension converted = converted();
+        for (final Category c : converted.categories) {
             final NumberRange<?> r = c.range;
             if (r instanceof MeasurementRange<?>) {
                 final Unit<?> unit = ((MeasurementRange<?>) r).unit();
@@ -301,7 +354,7 @@ public class SampleDimension implements Serializable {
                     if (main != null && !main.equals(unit)) {
                         throw new IllegalStateException();
                     }
-                    if (main == null || c == categories.converted.main) {
+                    if (main == null || c == converted.categories.main) {
                         main = unit;
                     }
                 }
@@ -311,6 +364,25 @@ public class SampleDimension implements Serializable {
     }
 
     /**
+     * Returns a sample dimension that describes real values or sample values, depending if {@code converted} is {@code true}
+     * or {@code false} respectively.  If there is no {@linkplain #getTransferFunction() transfer function}, then this method
+     * returns {@code this}.
+     *
+     * @param  converted  {@code true} for a sample dimension representing converted values,
+     *                    or {@code false} for a sample dimension representing sample values.
+     * @return a sample dimension representing converted or sample values, depending on {@code converted} argument value.
+     *         May be {@code this} but never {@code null}.
+     */
+    public SampleDimension forConvertedValues(final boolean converted) {
+        if (converse != null && transferFunction != null) {
+            if (transferFunction.isIdentity() != converted) {
+                return converse;
+            }
+        }
+        return this;
+    }
+
+    /**
      * Returns a hash value for this sample dimension.
      */
     @Override
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
index a8ca87e..39d9cb8 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleRangeFormat.java
@@ -81,7 +81,7 @@ final class SampleRangeFormat extends RangeFormat {
 
     /**
      * Computes the smallest number of fraction digits necessary to resolve all quantitative values.
-     * This method assumes that real values in the range {@code Category.converted.range} are stored
+     * This method assumes that real values in the range {@code Category.converse.range} are stored
      * as integer sample values in the range {@code Category.range}.
      */
     private void prepare(final List<Category> categories) {
@@ -89,7 +89,7 @@ final class SampleRangeFormat extends RangeFormat {
         hasPackedValues = false;
         hasQuantitative = false;
         for (final Category category : categories) {
-            final Category converted = category.converted;
+            final Category converted = category.converted();
             final boolean  isPacked  = (category.minimum != converted.minimum)
                                      | (category.maximum != converted.maximum);
             hasPackedValues |= isPacked;
@@ -237,9 +237,10 @@ final class SampleRangeFormat extends RangeFormat {
              * "Real values" column. Omitted if no category has a transfer function.
              */
             if (hasQuantitative) {
-                String text = formatMeasure(category.converted.range);            // Example: [6.0 … 25.0)°C
+                final Category converted = category.converted();
+                String text = formatMeasure(converted.range);               // Example: [6.0 … 25.0)°C
                 if (text == null) {
-                    text = String.valueOf(category.converted.getRangeLabel());    // Example: NaN #0
+                    text = String.valueOf(converted.getRangeLabel());       // Example: NaN #0
                 }
                 table.append(text);
                 table.nextColumn();
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
index 618ddc1..d367841 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
@@ -59,6 +59,16 @@ public final strictfp class CategoryListTest extends TestCase {
     }
 
     /**
+     * Asserts that the given categories defines range of sample values ({@literal i.e.} are not converse).
+     */
+    private static void assertNotConverted(final CategoryList categories) {
+        for (final Category c : categories) {
+            assertNotNull(c.range);
+            assertFalse(c.range instanceof ConvertedRange);
+        }
+    }
+
+    /**
      * Tests the checks performed by {@link CategoryList} constructor.
      */
     @Test
@@ -71,7 +81,7 @@ public final strictfp class CategoryListTest extends TestCase {
             new Category("Again",   NumberRange.create(10, true, 10, true), null, null, padValues)       // Range overlaps.
         };
         try {
-            assertTrue(new CategoryList(categories.clone(), null).isPublic());
+            assertNotConverted(new CategoryList(categories.clone(), null));
             fail("Should not have accepted range overlap.");
         } catch (IllegalArgumentException exception) {
             // This is the expected exception.
@@ -81,7 +91,7 @@ public final strictfp class CategoryListTest extends TestCase {
         }
         // Removes the wrong category. Now, construction should succeed.
         categories = Arrays.copyOf(categories, categories.length - 1);
-        assertTrue("isPublic", new CategoryList(categories, null).isPublic());
+        assertNotConverted(new CategoryList(categories, null));
         assertSorted(categories);
     }
 
@@ -167,18 +177,18 @@ public final strictfp class CategoryListTest extends TestCase {
     @Test
     public void testRanges() {
         final CategoryList list = new CategoryList(categories(), null);
-        assertTrue  ("isMinIncluded",            list.range.isMinIncluded());
-        assertFalse ("isMaxIncluded",            list.range.isMaxIncluded());
-        assertFalse ("converted.isMinIncluded",  list.converted.range.isMinIncluded());     // Because computed from maxValue before conversion.
-        assertFalse ("converted.isMaxIncluded",  list.converted.range.isMaxIncluded());
-        assertEquals("minValue",              0, ((Number) list.range          .getMinValue()).doubleValue(), STRICT);
-        assertEquals("maxValue",            120, ((Number) list.range          .getMaxValue()).doubleValue(), STRICT);
-        assertEquals("converted.minValue", -117, ((Number) list.converted.range.getMinValue()).doubleValue(), STRICT);
-        assertEquals("converted.maxValue",   15, ((Number) list.converted.range.getMaxValue()).doubleValue(), STRICT);
-        assertEquals("converted.minValue", -117, list.converted.range.getMinDouble(false), STRICT);
-        assertEquals("converted.maxValue",   15, list.converted.range.getMaxDouble(false), STRICT);
-        assertEquals("converted.minValue", -116, list.converted.range.getMinDouble(true),  CategoryTest.EPS);
-        assertEquals("converted.maxValue", 14.9, list.converted.range.getMaxDouble(true),  CategoryTest.EPS);
+        assertTrue  ("isMinIncluded",           list.range.isMinIncluded());
+        assertFalse ("isMaxIncluded",           list.range.isMaxIncluded());
+        assertFalse ("converse.isMinIncluded",  list.converse.range.isMinIncluded());     // Because computed from maxValue before conversion.
+        assertFalse ("converse.isMaxIncluded",  list.converse.range.isMaxIncluded());
+        assertEquals("minValue",             0, ((Number) list.range          .getMinValue()).doubleValue(), STRICT);
+        assertEquals("maxValue",           120, ((Number) list.range          .getMaxValue()).doubleValue(), STRICT);
+        assertEquals("converse.minValue", -117, ((Number) list.converse.range.getMinValue()).doubleValue(), STRICT);
+        assertEquals("converse.maxValue",   15, ((Number) list.converse.range.getMaxValue()).doubleValue(), STRICT);
+        assertEquals("converse.minValue", -117, list.converse.range.getMinDouble(false), STRICT);
+        assertEquals("converse.maxValue",   15, list.converse.range.getMaxDouble(false), STRICT);
+        assertEquals("converse.minValue", -116, list.converse.range.getMinDouble(true),  CategoryTest.EPS);
+        assertEquals("converse.maxValue", 14.9, list.converse.range.getMaxDouble(true),  CategoryTest.EPS);
     }
 
     /**
@@ -193,37 +203,37 @@ public final strictfp class CategoryListTest extends TestCase {
         /*
          * Checks category searches for values that are insides the range of a category.
          */
-        assertSame(  "0", categories[0],           list.search(  0));
-        assertSame(  "7", categories[1],           list.search(  7));
-        assertSame(  "3", categories[2],           list.search(  3));
-        assertSame(" 10", categories[3],           list.search( 10));
-        assertSame(" 50", categories[3],           list.search( 50));
-        assertSame("100", categories[4],           list.search(100));
-        assertSame("110", categories[4],           list.search(110));
-        assertSame(  "0", categories[0].converted, list.converted.search(MathFunctions.toNanFloat(  0)));
-        assertSame(  "7", categories[1].converted, list.converted.search(MathFunctions.toNanFloat(  7)));
-        assertSame(  "3", categories[2].converted, list.converted.search(MathFunctions.toNanFloat(  3)));
-        assertSame(" 10", categories[3].converted, list.converted.search(  /* transform( 10) */     6 ));
-        assertSame(" 50", categories[3].converted, list.converted.search(  /* transform( 50) */    10 ));
-        assertSame("100", categories[4].converted, list.converted.search(  /* transform(100) */   -97 ));
-        assertSame("110", categories[4].converted, list.converted.search(  /* transform(110) */  -107 ));
+        assertSame(  "0", categories[0],          list.search(  0));
+        assertSame(  "7", categories[1],          list.search(  7));
+        assertSame(  "3", categories[2],          list.search(  3));
+        assertSame(" 10", categories[3],          list.search( 10));
+        assertSame(" 50", categories[3],          list.search( 50));
+        assertSame("100", categories[4],          list.search(100));
+        assertSame("110", categories[4],          list.search(110));
+        assertSame(  "0", categories[0].converse, list.converse.search(MathFunctions.toNanFloat(  0)));
+        assertSame(  "7", categories[1].converse, list.converse.search(MathFunctions.toNanFloat(  7)));
+        assertSame(  "3", categories[2].converse, list.converse.search(MathFunctions.toNanFloat(  3)));
+        assertSame(" 10", categories[3].converse, list.converse.search(  /* transform( 10) */     6 ));
+        assertSame(" 50", categories[3].converse, list.converse.search(  /* transform( 50) */    10 ));
+        assertSame("100", categories[4].converse, list.converse.search(  /* transform(100) */   -97 ));
+        assertSame("110", categories[4].converse, list.converse.search(  /* transform(110) */  -107 ));
         /*
          * Checks values outside the range of any category. For direct conversion, no category shall be returned.
          * For inverse conversion, the nearest category shall be returned.
          */
-        assertNull( "-1",                          list.search( -1));
-        assertNull(  "2",                          list.search(  2));
-        assertNull(  "4",                          list.search(  4));
-        assertNull(  "9",                          list.search(  9));
-        assertNull("120",                          list.search(120));
-        assertNull("200",                          list.search(200));
-        assertNull( "-1",                          list.converted.search(MathFunctions.toNanFloat(-1)));    // Nearest sample is 0
-        assertNull(  "2",                          list.converted.search(MathFunctions.toNanFloat( 2)));    // Nearest sample is 3
-        assertNull(  "4",                          list.converted.search(MathFunctions.toNanFloat( 4)));    // Nearest sample is 3
-        assertNull(  "9",                          list.converted.search(MathFunctions.toNanFloat( 9)));    // Nearest sample is 10
-        assertSame(  "9", categories[3].converted, list.converted.search( /* transform(  9) */   5.9 ));    // Nearest sample is 10
-        assertSame("120", categories[4].converted, list.converted.search( /* transform(120) */  -117 ));    // Nearest sample is 119
-        assertSame("200", categories[4].converted, list.converted.search( /* transform(200) */  -197 ));    // Nearest sample is 119
+        assertNull( "-1",                         list.search( -1));
+        assertNull(  "2",                         list.search(  2));
+        assertNull(  "4",                         list.search(  4));
+        assertNull(  "9",                         list.search(  9));
+        assertNull("120",                         list.search(120));
+        assertNull("200",                         list.search(200));
+        assertNull( "-1",                         list.converse.search(MathFunctions.toNanFloat(-1)));    // Nearest sample is 0
+        assertNull(  "2",                         list.converse.search(MathFunctions.toNanFloat( 2)));    // Nearest sample is 3
+        assertNull(  "4",                         list.converse.search(MathFunctions.toNanFloat( 4)));    // Nearest sample is 3
+        assertNull(  "9",                         list.converse.search(MathFunctions.toNanFloat( 9)));    // Nearest sample is 10
+        assertSame(  "9", categories[3].converse, list.converse.search( /* transform(  9) */   5.9 ));    // Nearest sample is 10
+        assertSame("120", categories[4].converse, list.converse.search( /* transform(120) */  -117 ));    // Nearest sample is 119
+        assertSame("200", categories[4].converse, list.converse.search( /* transform(200) */  -197 ));    // Nearest sample is 119
     }
 
     /**
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
index bf0ab7a..9c1e2b5 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryTest.java
@@ -83,10 +83,10 @@ public final strictfp class CategoryTest extends TestCase {
             assertTrue("Allocated NaN ordinal", padValues.contains(sample));
             assertBoundEquals("range.minValue", sample, category.range.getMinValue());
             assertBoundEquals("range.maxValue", sample, category.range.getMaxValue());
-            final MathTransform1D inverse = category.converted.transferFunction;
+            final MathTransform1D inverse = category.converse.toConverse;
             for (int i=0; i<4; i++) {
                 final float x = 100 * random.nextFloat();
-                final float y = (float) category.transferFunction.transform(x);
+                final float y = (float) category.toConverse.transform(x);
                 assertTrue("isNaN", Float.isNaN(y));
                 final int ordinal = MathFunctions.toNanOrdinal(y);
                 if (collision) {
@@ -115,19 +115,19 @@ public final strictfp class CategoryTest extends TestCase {
             final Category category = new Category("Random", NumberRange.create(lower, true, upper, true),
                     (MathTransform1D) MathTransforms.linear(scale, offset), null, Collections.emptySet());
 
-            assertBoundEquals("range.minValue",     lower,              category.range.getMinValue());
-            assertBoundEquals("range.maxValue",     upper,              category.range.getMaxValue());
-            assertBoundEquals("converted.minValue", lower*scale+offset, category.converted.range.getMinValue());
-            assertBoundEquals("converted.maxValue", upper*scale+offset, category.converted.range.getMaxValue());
+            assertBoundEquals("range.minValue",    lower,              category.range.getMinValue());
+            assertBoundEquals("range.maxValue",    upper,              category.range.getMaxValue());
+            assertBoundEquals("converse.minValue", lower*scale+offset, category.converse.range.getMinValue());
+            assertBoundEquals("converse.maxValue", upper*scale+offset, category.converse.range.getMaxValue());
 
-            final MathTransform1D inverse = category.converted.transferFunction;
-            assertSame("inverse", inverse, category.transferFunction.inverse());
+            final MathTransform1D inverse = category.converse.toConverse;
+            assertSame("inverse", inverse, category.toConverse.inverse());
 
             for (int i=0; i<20; i++) {
                 final double x = 100 * random.nextDouble();
                 final double y = x*scale + offset;
-                assertEquals("transferFunction", y, category.transferFunction.transform(x), EPS);
-                assertEquals("inverse",          x, inverse.transform(y), EPS);
+                assertEquals("toConverse", y, category.toConverse.transform(x), EPS);
+                assertEquals("inverse",    x, inverse.transform(y), EPS);
             }
         }
     }


Mime
View raw message