1
\$\begingroup\$

I need to select a color given a position within a range.

Using the "bwr" colormap shown below, I want to return a blue color for a value at the beginning of the range, a red color for a value at the end of the range, and a suitable interpolated value for values in between.

color maps

The lower and upper ends of the range can be anything. From negative to positive numbers, for example [-0.5,0.5] or [0,1] or [-0.2 to 0.6].

Here is my code:

#include <opencv2/core/core.hpp>

cv::Scalar get_colour(double v, double vmin, double vmax)
{
    double r, g, b = 0;
    double dv;

    if (v < vmin)
        v = vmin;
    if (v > vmax)
        v = vmax;
    dv = vmax - vmin;

    if (v < (vmin + 0.5 * dv)) {
        r = 2 * (v - vmin) / dv;
        g = 2 * (v - vmin) / dv;
        b = 1;
    }
    else if (v >= (vmin + 0.5 * dv)) {
        b = 1 + 2 * (vmin + 0.5 * dv - v) / dv;
        g = 1 + 2 * (vmin + 0.5 * dv - v) / dv;
        r = 1;
    }

    return cv::Scalar(b * 255, g * 255, r * 255);
}

Can you check if this function always work good, no matter what range and value I choose, and what can I write better?

\$\endgroup\$
  • \$\begingroup\$ You'll receive better reviews the more complete the code you show. For example, I recommend that you show the necessary #include lines, and a main() that shows how to call your function. It's not mandatory, but it really helps! \$\endgroup\$ – Toby Speight Jul 7 '17 at 8:11
  • 1
    \$\begingroup\$ I've re-written your description to more fluent English - please check that I haven't misunderstood anything you wrote, and fix it if I have. I hope I've helped! \$\endgroup\$ – Toby Speight Jul 7 '17 at 8:18
3
\$\begingroup\$

The logic is mostly clear, but it's not obvious exactly how to change to one of the other scales. There's also a case where calling the function wrongly (with vmin==vmax), you'll divide by zero. Although that's a bug in the calling code, it may be preferable to take a defensive line on this, and just return the middle value.

I'd be inclined to split this into two three parts:

  1. scale the input value to the range [0,1]
  2. split into one of the sub-ranges [0,0.5) or [0.5,1]
  3. interpolate linearly between the two colours

I'll start with the last of those. Assuming that the input is between 0 and 1, we can interpolate quite easily:

cv::Scalar get_colour(double alpha, const cv::Scalar& c0, const cv::Scalar& c1)
{
    return (1-alpha) * c0  +  alpha * c1;
}

How do we get that value between 0 and 1? Steps 1 and 2 look like

cv::Scalar get_colour(double v, double vmin, double vmax)
{
    // clamp value within range
    v = v <= vmin ? vmin
        : v >= vmax ? vmax
        : v;

    const double alpha = (vmax <= vmin)
        ? 0.5                   // avoid divide by zero
        : (v - vmin) / (vmax - vmin);

    static const cv::Scalar blue{ 255, 0, 0 };
    static const cv::Scalar white{ 255, 255, 255 };
    static const cv::Scalar red{ 0, 0, 255 };

    if (alpha < 0.5) {
        return get_colour(alpha * 2, blue, white);
    } else {
        return get_colour(alpha * 2 - 1, white, red);
    }
}

Here, we have used constants for the colours in both parts of the gradient, meaning that if we want to change to different colours, there's only one place that needs changing.

I hope it's apparent with this change that we could make the interpolation into an object, with the range and colours being member data. Depending on your application, that may be easier to work with than free functions with lots of parameters.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.