Convert double value to RGB Color in c#

10,582

Solution 1

In one of your comments you said: "no My intention is to include all the colors and I dont want to favor any of them. Simply I would like the best way to convert a double value to an RGB color"

So you don't care about what the actual relationship is between the double and the Color and you don't want to operate on the double values in a way which is somehow consistent with their Color counterparts. In that case, things are easier than you expected.

Might I remind you that an RGB colour is composed of 3 bytes, although, for combinatorial reasons, the .NET BCL class Color offers the 3 components as int values.

So you have 3 bytes ! A double occupies 8 bytes. If my assumption is correct, at the end of this answer you might be considering float as a better candidate (if a smaller footprint is important for you, of course).

Enough chit chat, on to the actual problem. The approach I'm about to lay out is not so much linked with mathematics as it is with memory management and encoding.

Have you heard about the StructLayoutAttribute attribute and it's entourage, the FieldOffsetAttribute attribute ? In case you haven't you will probably be awed by them.

Say you have a struct, let's call it CommonDenominatorBetweenColoursAndDoubles. Let's say it contains 4 public fields, like so:

public struct CommonDenominatorBetweenColoursAndDoubles {
    public byte R;
    public byte G;
    public byte B;

    public double AsDouble;
}

Now, say you want to orchestrate the compiler and imminent runtime in such a way so the R, the G and the B fields (each of which take up 1 byte) are laid out contiguously and that the AsDouble field overlaps them in it's first 3 bytes and continues with it's own, exclusively remaining 5 bytes. How do you do that ?

You use the aforementioned attributes to specify:

  1. The fact that you're taking control of the struct's layout (be careful, with great power comes great responsibility)
  2. The facts that R, G and B start at the 0th, 1st and 2nd bytes within the struct (since we know that byte occupies 1 byte) and that AsDouble also starts at the 0th byte, within the struct.

The attributes are found in mscorlib.dll under the System.Runtime.InteropServices namespace and you can read about them here StructLayout and here FieldOffset.

So you can achieve all of that like so:

[StructLayout(LayoutKind.Explicit)]
public struct CommonDenominatorBetweenColoursAndDoubles {

    [FieldOffset(0)]
    public byte R;
    [FieldOffset(1)]
    public byte G;
    [FieldOffset(2)]
    public byte B;

    [FieldOffset(0)]
    public double AsDouble;

}

Here's what the memory within an instance of the struct (kinda) looks like:

Diagram showing memory details of converter struct

And what better way to wrap it all up than a couple of extension methods:

public static double ToDouble(this Color @this) {

    CommonDenominatorBetweenColoursAndDoubles denom = new CommonDenominatorBetweenColoursAndDoubles ();

    denom.R = (byte)@this.R;
    denom.G = (byte)@this.G;
    denom.B = (byte)@this.B;

    double result = denom.AsDouble;
    return result;

}

public static Color ToColor(this double @this) {

    CommonDenominatorBetweenColoursAndDoubles denom = new CommonDenominatorBetweenColoursAndDoubles ();

    denom.AsDouble = @this;

    Color color = Color.FromArgb (
        red: denom.R,
        green: denom.G,
        blue: denom.B
    );
    return color;

}

I also tested this to make sure it's bullet-proof and by what I can tell, you won't have to worry about a thing:

for (int x = 0; x < 255; x++) {
for (int y = 0; y < 255; y++) {
for (int z = 0; z < 255; z++) {

    var c1 = Color.FromArgb (x, y, z);
    var d1 = c1.ToDouble ();
    var c2 = d1.ToColor ();

    var x2 = c2.R;
    var y2 = c2.G;
    var z2 = c2.B;

    if ((x != x2) || (y != y2) || (z != z2))
        Console.Write ("1 error");

}
}
}

This completed without yielding any errors.

EDIT

Before I begin the edit: If you study the double encoding standard a bit (which is common between all languages, frameworks and most probably most processors) you will come to the conclusion (which I also tested) that by iterating through all combinations of the 3 least significant bytes (the 24 least significant bits) of an 8 byte double, which is what we're doing right here, you will end up with double values which are mathematically bounded by 0 at the lower end and double.Epsilon * (256 * 3 - 1) at the other end (inclusively). That is true, of course, if the remaining more significant 5 bytes are filled with 0s.

In case it's not clear already, double.Epsilon * (256 * 3 - 1) is an incredibly small number which people can't even pronounce. Your best shot at the pronunciation would be: It's the product between 2²⁴ and the smallest positive double greater than 0 (which is immensely small) or if it suits you better: 8.28904556439245E-317.

Within that range you will discover you have precisely 256 * 3 which is 2²⁴ "consecutive" double values, which start with 0 and are separated by the smallest double distance possible.

By means of mathematical (logical value) manipulation (not by direct memory addressing) you can easily stretch that range of 2²⁴ numbers from the original 0 .. double.Epsilon * (2²⁴ - 1) to 0 .. 1.

This is what I'm talking about:

Linear transformation

Don't mistake double.Epsilon ( or ε) for the exponential letter e. double.Epsilon is somehow a representation of it's calculus counterpart, which could mean the smallest real number which is greater than 0.

So, just to make sure we're ready for the coding, let's recap what's going on in here:

We have N (N being 2²⁴) double numbers starting from 0 and ending in ε * (N-1) (where ε, or double.Epsilon is smallest double greater than 0).

In a sense, the struct we've created is really just helping us to do this:

double[] allDoubles = new double[256 * 256 * 256];
double cursor = 0;
int index = 0;

for (int r = 0; r < 256; r++)
for (int g = 0; g < 256; g++)
for (int b = 0; b < 256; b++) {

  allDoubles[index] = cursor;

  index++;
  cursor += double.Epsilon;

}

So then, why did we go through all that trouble with the struct ? Because it's a lot faster, because it does not involve any mathematical operations, and we're able to access randomly anyone of the N values, based on the R, G and B inputs.

Now, on to the linear transformation bit.

All we have to do now is a bit of math (which will take a bit longer to compute since it involves floating point operations but will successfully stretch our range of doubles to an equally distributed one between 0 and 1):

Within the struct we created earlier, we're going to rename the AsDouble field, make it private and create a new property called AsDouble to handle the transformation (both ways):

[StructLayout(LayoutKind.Explicit)]
public struct CommonDenominatorBetweenColoursAndDoubles {

    [FieldOffset(0)]
    public byte R;
    [FieldOffset(1)]
    public byte G;
    [FieldOffset(2)]
    public byte B;

    // we renamed this field in order to avoid simple breaks in the consumer code
    [FieldOffset(0)]
    private double _AsDouble;

    // now, a little helper const
    private const int N_MINUS_1 = 256 * 256 * 256 - 1;

    // and maybe a precomputed raw range length
    private static readonly double RAW_RANGE_LENGTH = double.Epsilon * N_MINUS_1;

    // and now we're adding a property called AsDouble
    public double AsDouble {
        get { return this._AsDouble / RAW_RANGE_LENGTH; }
        set { this._AsDouble = value * RAW_RANGE_LENGTH; }
    }

}

You will be pleasantly surprised to learn that the tests I proposed before this EDIT are still working fine, with this new addition, so you have 0% loss of information and now the range of doubles is equally stretched across 0 .. 1.

Solution 2

As mentioned above in the comments the formulas you've drawn do not satisfy your condition of uniformly spanning the whole color range. I believe this should work (not the only possible solution by far):

*Edit: fixed the formula, the previous did not generate all possible colors

int red = Math.Min((int)(X * 256), 255);
int green = Math.Min((int)((X * 256 - red) * 256), 255);
int blue = Math.Min((int)(((X * 256 - red) * 256 - green) * 256), 255);

Math.Min is employed to fix the border scenario as X -> 1D

Share:
10,582
Drill
Author by

Drill

Updated on June 05, 2022

Comments

  • Drill
    Drill almost 2 years

    I am trying to convert a double value (which is between 0 and 1) to RGB Color. In the code below you can see what Im trying to do but I feel that there is something wrong with this algorithm. Im not getting all the colors. Maybe there is a loose of information when I convert from double to int or Im not sure...but please have a look at it and if you have any suggestion or any other method(validated one) please let me know:

        private Color generateRGB(double X)
        {
            Color color;
            if (X >= 0.5) //red and half of green colors
            {
                int Red = (int)((2 * X - 1) * 255);
                int Green = (int)((2 - 2 * X) * 255);
                int Blue = 0;
                color = Color.FromArgb(Red, Green, Blue);
            }
            else  // blue and half of green colors
            {
                int Red = 0;
                int Green = (int)((2 * X) * 255);
                int Blue = (int)((1 - 2 * X) * 255);
                color = Color.FromArgb(Red, Green, Blue);
            }
            return color;
        }
    

    Here is the image that expresses the best what im trying to do.

    https://www.dropbox.com/s/bvs3a9m9nc0rk5e/20131121_143044%20%281%29.jpg

    [Updated]

    That`s how I did and it seems a good solution. Please have a look at it and tell me weather this is better representation or not (maybe those who have a better knowledge for Color Spaces could give a feedback about it)

    I have used an HSVtoRGB conversion algorithm from here: http://www.splinter.com.au/converting-hsv-to-rgb-colour-using-c/. Knowing that my values are at [0,1] interval, Im extending this interval to [0, 360] in order to use the algorithm for converting HSV to RGB. Im using s and v equal to 1 in my case. Here is the code for better explanation.

            private Color generateRGB(double X)
            {
                Color color;
    
                int red;
                int green;
                int blue;
                HsvToRgb(X*360,1,1,out red,out green,out blue);
    
                color = Color.FromArgb(red, green, blue);
    
                return color;
            }