在您的一个 cmets 中,您说:“不,我的意图是包含所有颜色,我不想偏爱其中任何一种。只是我想要将双精度值转换为 RGB 颜色的最佳方法”
因此,您不必关心double 和Color 之间的实际关系,并且您不想以某种与其@987654332 一致的方式对double 值进行操作@同行。
在这种情况下,事情比你想象的要容易。
我可以提醒您,RGB 颜色由 3 个字节组成,尽管出于组合原因,.NET BCL 类 Color 提供 3 个组件作为 int 值。
所以你有 3 个字节!
一个double 占用8 个字节。
如果我的假设是正确的,那么在这个答案的最后,您可能会考虑将 float 作为更好的候选者(当然,如果较小的足迹对您很重要)。
闲聊已经够了,进入实际问题。
我将要介绍的方法与其说是与数学有关,不如说是与内存管理和编码有关。
您听说过StructLayoutAttribute 属性及其随行人员FieldOffsetAttribute 属性吗?
如果你没有,你可能会被他们敬畏。
假设你有一个结构,我们称之为CommonDenominatorBetweenColoursAndDoubles。
假设它包含 4 个公共字段,如下所示:
public struct CommonDenominatorBetweenColoursAndDoubles {
public byte R;
public byte G;
public byte B;
public double AsDouble;
}
现在,假设您想以这样一种方式编排编译器和即将到来的运行时,以便将 R、G 和 B 字段(每个字段占用 1 个字节)连续布局,并且AsDouble 字段在前 3 个字节中与它们重叠,并继续使用它自己的,仅剩下 5 个字节。你是怎么做到的?
您使用上述属性来指定:
- 您正在控制
struct 的布局这一事实(小心,权力越大责任越大)
-
R、G和B从struct内的第0、第1和第2个字节开始(因为我们知道byte占用1个字节)并且AsDouble也开始struct 内的第 0 个字节。
属性位于System.Runtime.InteropServices 命名空间下的mscorlib.dll 中,您可以在此处StructLayout 和此处FieldOffset 阅读有关它们的信息。
所以你可以像这样实现所有这些:
[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;
}
这是struct(有点)实例中的内存的样子:
还有什么比几个扩展方法更好的方法来包装它:
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;
}
我还对此进行了测试,以确保它是防弹的,据我所知,您不必担心任何事情:
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");
}
}
}
这完成了,没有产生任何错误。
编辑
在我开始编辑之前:如果您稍微研究一下double encoding standard(这在所有语言、框架和很可能大多数处理器之间都很常见),您将得出结论(我也测试过),通过遍历所有8 字节双精度的 3 个最低有效字节(24 个最低有效位)的组合,这就是我们在这里所做的,您最终将得到 double 值,这些值在数学上以 0 为界和double.Epsilon * (256 * 3 - 1) 在另一端(包括)。当然,如果剩余的更重要的 5 个字节用0s 填充,那是正确的。
如果还不清楚,double.Epsilon * (256 * 3 - 1) 是一个令人难以置信的小数字,人们甚至无法发音。
你最好的发音是:它是2²⁴ 和最小正数double 之间的乘积,大于0(非常小),或者如果它更适合你:8.28904556439245E-317。
在该范围内,您会发现您恰好有256 * 3,即2²⁴“连续”double 值,它们以0 开头,并由可能的最小double 距离隔开。
通过数学(逻辑值)操作(而不是通过直接内存寻址),您可以轻松地将 2²⁴ 数字的范围从原始 0 .. double.Epsilon * (2²⁴ - 1) 扩展到 0 .. 1。
这就是我要说的:
不要将double.Epsilon(或ε)误认为是指数字母e。
double.Epsilon 在某种程度上代表了它的微积分对应物,这可能意味着大于 0 的最小实数。
所以,为了确保我们为编码做好准备,让我们回顾一下这里发生了什么:
我们有N(N 是2²⁴)double 数字从0 开始并以ε * (N-1) 结尾(其中ε 或double.Epsilon 是最小的double 大于@ 987654391@).
从某种意义上说,我们创建的struct 真的只是帮助我们做到这一点:
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;
}
那么,为什么我们要在 struct 上遇到这么多麻烦呢?
因为它要快得多,因为它不涉及任何数学运算,而且我们能够根据R、G 和B 输入随机访问N 值中的任何一个。
现在,进入线性变换位。
我们现在要做的只是一点数学运算(这将需要更长的时间来计算,因为它涉及浮点运算,但会成功地将我们的双打范围扩展到0 和1 之间的平均分布):
在我们之前创建的struct 中,我们将重命名AsDouble 字段,将其设为私有并创建一个名为AsDouble 的新属性来处理转换(两种方式):
[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; }
}
}
您会惊喜地发现,我在此EDIT 之前提出的测试仍然可以正常工作,有了这个新增功能,因此您的信息丢失率为 0%,现在双打范围为同样横跨0 .. 1。