Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Changing the range scale of values of arbitrary numeric types

I have a need to convert a collection of numbers from one range to another, while keeping the relative distribution of the values.

For example, a vector containing randomly-generated floats could be scaled to fit into possible unsigned char values (0..255). Ignoring the type conversion, this would mean that whatever input was provided (e.g. -1.0 to 1.0) , all numbers would be scaled to 0.0 to 255.0 (or thereabouts).

I have created a template class to perform this conversion, which can be applied to a collection using std::transform:

template <class TYPE>
class scale_value {
    const TYPE fmin, tmin, ratio;
public:
    TYPE operator()(const TYPE& v) {
        TYPE vv(v);
        vv += (TYPE(0) - fmin); // offset according to input minimum
        vv *= ratio;            // apply the scaling factor
        vv -= (TYPE(0) - tmin); // offset according to output minimum
        return vv;
    }
    // constructor takes input min,max and output min,max
    scale_value(const TYPE& pfmin, const TYPE& pfmax, const TYPE& ptmin, const TYPE& ptmax)
        : fmin(pfmin), tmin(ptmin), ratio((ptmax-tmin)/(pfmax-fmin)) { }
    // some code removed for brevity
};

However, the above code only works correctly for real numbers (float, double, ...). Integers work when scaling up, but even then only by whole ratios:

float scale_test_float[] = {0.0, 0.5, 1.0, 1.5, 2.0};
int scale_test_int[] = {0, 5, 10, 15, 20};

// create up-scalers
scale_value<float> scale_up_float(0.0, 2.0, 100.0, 200.0);
scale_value<int> scale_up_int(0, 20, 100, 200);

// create down-scalers
scale_value<float> scale_down_float(100.0, 200.0, 0.0, 2.0);
scale_value<int> scale_down_int(100, 200, 0, 20);

std::transform(scale_test_float, scale_test_float+5, scale_test_float, scale_up_float);
// scale_test_float -> 100.0, 125.0, 150.0, 175.0, 200.0
std::transform(scale_test_int, scale_test_int+5, scale_test_int, scale_up_int);
// scale_test_int -> 100, 125, 150, 175, 200

std::transform(scale_test_float, scale_test_float+5, scale_test_float, scale_down_float);
// scale_test_float -> 0.0, 0.5, 1.0, 1.5, 2.0
std::transform(scale_test_int, scale_test_int+5, scale_test_int, scale_down_int);
// scale_test_int -> 0, 0, 0, 0, 0 : fails due to ratio being rounded to 0

My current solution to this issue is to store everything internal to scale_value as a double, and use type conversion as needed:

TYPE operator()(const TYPE& v) {
    double vv(static_cast<double>(v));
    vv += (0.0 - fmin);                // offset according to input minimum
    vv *= ratio;                       // apply the scaling factor
    vv -= (0.0 - tmin);                // offset according to output minimum
    return static_cast<TYPE>(vv);
}

This works for most cases, albeit with some errors with integers, as the values are truncated rather than rounded. For example scaling {0,5,10,15,20} from 0..20 to 20..35 and then back gives {0,4,9,14,20}.

So, my question is, is there a better method of doing this? In the case of scaling a collection of floats, the type-conversions seem rather redundant, whereas when scaling ints there are errors introduced due to truncation.

As an aside, I was surprised not to spot something (at least, nothing obvious) in boost for this purpose. Maybe I missed it - the various maths libraries confuse me.

Edit: I realize I could specialize operator() for specific types, however this would mean a lot of code-duplication, which defeats one of the useful parts of templates. Unless there is a method to, say, specialize once for all non-float types (short, int, uint, ...).

like image 710
icabod Avatar asked Feb 21 '13 13:02

icabod


3 Answers

First I think that your ratio probably needs to be some floating point type and computed using floating-point division (possibly another mechanism would work too). Otherwise if you try for example to scale from [0, 19] to [0, 20] you'll wind up with an integral ratio of 1 and perform no scaling whatsoever!

Next, let's assume that things work fine for floating-point types. Now we'll just do all our math as double, but if the output type is integral we'd like to round to the closest output integer rather than truncating down. So we can use is_integral to force some rounding into place (note I don't have access to compile/test this right now):

TYPE operator()(const TYPE& v)
{
    double vv(static_cast<double>(v));
    vv -= fmin;                // offset according to input minimum
    vv *= ratio;               // apply the scaling factor
    vv += tmin;                // offset according to output minimum
    return static_cast<TYPE>(vv + (0.5 * is_integral<TYPE>::value));  // Round for integral types
}
like image 184
Mark B Avatar answered Oct 12 '22 22:10

Mark B


Following @John R. Strohm's suggestion, which would work for Integers, I have come up with the following, which seems to work with only a need to provide two specializations of the class (my concern was having to write a specialization for every type). It does require writing a "trait" for each non-whole type, however.

First I create a "traits"-style class (note that in C++11 I think this is already provided in std::is_floating_point, but for now I'm stuck with vanilla C++):

template <class NUMBER>
struct number_is_float { static const bool val = false; };

template<>
struct number_is_float<float> { static const bool val = true; };

template<>
struct number_is_float<double> { static const bool val = true; };

template<>
struct number_is_float<long double> { static const bool val = true; };

Using this traits-style class, we can provide a basic "whole number" implementation of the scale_value class:

template <class TYPE, bool IS_FLOAT=number_is_float<TYPE>::val>
class scale_value
{
private:
    const double fmin, tmin, ratio;
public:
    TYPE operator()(const TYPE& v) {
        double vv(static_cast<double>(v));
        vv += (0.0 - fmin);
        vv *= ratio;
        vv += 0.5 * ((static_cast<double>(v) >= 0.0) ? 1.0 : -1.0);
        vv -= (0.0 - tmin);
        return static_cast<TYPE>(vv);
    }
    scale_value(const TYPE& pfmin, const TYPE& pfmax, const TYPE& ptmin, const TYPE& ptmax)
        : fmin(static_cast<double>(pfmin))
        , tmin(static_cast<double>(ptmin))
        , ratio((static_cast<double>(ptmax)-tmin)/(static_cast<double>(pfmax)-fmin))
    {
    }
};

...and a partial specialization for cases where the TYPE parameter has a "trait" that says it's a float of some kind:

template <class TYPE>
class scale_value<TYPE, true>
{
private:
    const TYPE fmin, tmin, ratio;
public:
    TYPE operator()(const TYPE& v) {
        TYPE vv(v);
        vv += (TYPE(0.0) - fmin);
        vv *= ratio;
        vv -= (TYPE(0.0) - tmin);
        return vv;
    }
    scale_value(const TYPE& pfmin, const TYPE& pfmax, const TYPE& ptmin, const TYPE& ptmax)
        : fmin(pfmin), tmin(ptmin), ratio((ptmax-tmin)/(pfmax-fmin)) {}
};

The main differences between these classes is that with the whole-number implementation, the data in the classes is stored as a double, and there is built-in rounding as per John's answer.

If I decided I needed to implement a fixed-point class, then I guess I would need to add this as another trait.

like image 42
icabod Avatar answered Oct 12 '22 23:10

icabod


Rounding is your responsibility, not the computer's/compiler's.

In your operator(), you need to provide a "rounding bit" in the multiplication.

I'd try starting with something like:

TYPE operator()(const TYPE& v) {
    double vv(static_cast<double>(v));
    vv += (0.0 - fmin);                // offset according to input minimum
    vv *= ratio;                       // apply the scaling factor
    vv += SIGN(static_cast<double>(v))*0.5;
    vv -= (0.0 - tmin);                // offset according to output minimum
    return static_cast<TYPE>(vv);
}

You'll have to define a SIGN(x) function, if your compiler doesn't already provide one.

double SIGN(const double x) {
    return (x >= 0) ? 1.0 : -1.0;
}
like image 36
John R. Strohm Avatar answered Oct 12 '22 21:10

John R. Strohm