3

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;
        }
12
  • 2
    So RGB colors have 3 independent axes. A double has only 1. Which path through the color cube are you trying to get?
    – AShelly
    Commented Nov 21, 2013 at 12:19
  • Knowing that if the value X is between 0.5 to 1 it can be any color with blue equal to 0. Otherwise it will be any color with red equal to 0.
    – Drill
    Commented Nov 21, 2013 at 12:27
  • Knowing that if the value X is between 0.5 to 1 it can be any color with blue equal to 0. Otherwise it will be any color with red equal to 0. It is not easy to understand this. Try to formulate this better, maybe then you can write correct code.
    – Alex F
    Commented Nov 21, 2013 at 12:30
  • 3
    You'll e.g. never get white as a color with your code, because either blue is 0 or red is 0. Like AShelly say, we'd need to know at least some boundaries of the expected behavior. Commented Nov 21, 2013 at 12:36
  • 1
    Also - the formulas you've provided on your drawings have a tendency to favor green tints - is that your intention?
    – decPL
    Commented Nov 21, 2013 at 12:37

2 Answers 2

12

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.

13
  • With all these excellent explanations I have to accept this answer even though I also HAVE TO accept that I wanted something different (something that I couldn explain quit well in the explenation). Your answer is right because my question maybe was not enough clear :S. Thank You indeed for this answer. I really learned some great things from this answer and Im going to use these later
    – Drill
    Commented Nov 24, 2013 at 15:34
  • maybe the best way to explain my needs is using some results taken from matlab...and those results I want to achieve also in C#. In matlab if I have a matrix od double values between 0 and 1 I can easily show it as an HSV image. Example: im = [0 .5 1; .7 .6 .2; .9 .3 .4]; cm = colormap('hsv'); cdata = interp1(linspace(0,1,length(cm)),cm,im); figure;image(cdata). Using a matrix(or array of 256 double values) I am achieving this result: dropbox.com/s/d6dnbyfzgookjl1/… As you can see it is a 16 X 16 map with nodes having values between 0 and 1.
    – Drill
    Commented Nov 24, 2013 at 15:37
  • ...I achieved this even with c# by using HSVtoRGB algorithm explained on my question. I want exactly the same thing but not with HSV. I want it with RGB color space. because HSV doesnt include some of the colors. I hope im clear...and if im not then please just skip evertyhing I asked here and sorry for inconveniences.
    – Drill
    Commented Nov 24, 2013 at 15:44
  • With SOM there has to be a relation between double values that are close to each other (EX 0.001 and 0.002) and this relation between two close values should also be mapped when representing nodes(each node has its own double value) using colors...but with your code I couldnt achieve it. I know there is a way to achieve it because people did it before me and I am researching on how to do it but still the best result is usgin HSVtoRGB algorithm with H = double value, S = 1 and V = 1;
    – Drill
    Commented Nov 24, 2013 at 15:49
  • Using double values from 0...256 (0, 1 ,2 ,3 ,4 ,5...256) I got this result from matlab:dropbox.com/s/d6dnbyfzgookjl1/… ...and using your code I got this result...dropbox.com/s/nf684xeialxn52g/…
    – Drill
    Commented Nov 24, 2013 at 15:57
1

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

9
  • Im going to try it. Thank You
    – Drill
    Commented Nov 21, 2013 at 12:52
  • Unofrtunately I have tried this but still not all the colors are shown in the map of nodes (I know from double values that there should be some other colors apart from the ones that are shown using this formula :S). In a similar implementation in matlab I see there more colors than in my application but I dont know how they did it...
    – Drill
    Commented Nov 21, 2013 at 15:02
  • Please disregard the previous version, not enough sleep. This should work correctly now.
    – decPL
    Commented Nov 21, 2013 at 16:11
  • Well there is something wrong definitely because still it is not working. Im sure that should be a way but still I couldn find one. I was hoping that here someone can know such a way...
    – Drill
    Commented Nov 21, 2013 at 18:02
  • How exactly is this 'not working'? You claim there are colors that are not represented by this? Could you determine their RGB value? Also - are you certain the numbers do not have some specific distribution that might be important here?
    – decPL
    Commented Nov 22, 2013 at 8:21

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