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:
- The fact that you're taking control of the
struct
's layout (be careful, with great power comes great responsibility)
- 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:

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 0
s.
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:

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
.