在您的评论中,您说:“我的意图不是包含所有颜色,我不想偏袒任何一种颜色。我只想知道将双精度值转换为RGB颜色的最佳方法。”因此,您并不关心双精度和颜色之间的实际关系,也不想以某种与它们的颜色对应物相一致的方式操作双精度值。在这种情况下,事情比您预期的要简单。
我可以提醒您的是,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
字段在其前三个字节中重叠,并继续使用其自己的独有的5个字节。如何实现这一点?
您可以使用上述属性来指定:
1. 您正在控制
struct
的布局(请小心,伴随着强大的力量而来的是巨大的责任)
2.
R
、
G
和
B
从
struct
的第0、1和2个字节开始(因为我们知道
byte
占用1字节),
AsDouble
也从
struct
的第0个字节开始。
这些属性可以在mscorlib.dll
中的System.Runtime.InteropServices
名称空间下找到,您可以在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
实例内存的大致结构:
![Diagram showing memory details of converter struct](https://istack.dev59.com/AfziY.webp)
最好的结束方式莫过于使用几个扩展方法:
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
编码标准(这是所有语言、框架和大多数处理器之间通用的),您会得出结论(我也测试过):通过迭代8字节双精度浮点数的最低有效字节(即24个最低有效位)的所有组合,这正是我们在这里做的,您将得到数学上被限制在下界为0
,上界为double.Epsilon * (256 * 3 - 1)
(包括边界)的double
值。当然,这是在剩余的更重要的5个字节填充了0
的情况下成立。
如果还不清楚的话,
double.Epsilon * (256 * 3 - 1)
是一个无法被人们发音的极小数。
它的发音最好是这样的:它是
2²⁴
和大于
0
的最小正
double
的乘积(这个数非常小),或者更适合您的是:
8.28904556439245E-317
。
在这个范围内,您会发现您恰好有
256 * 3
个"连续"的
double
值,从
0
开始,以最小的
double
距离分隔。
通过数学(逻辑值)操作(而不是直接内存寻址),您可以轻松地将这个
2²⁴
数值范围从原来的
0 .. double.Epsilon * (2²⁴ - 1)
扩展到
0 .. 1
。
这就是我所说的。
![Linear transformation](https://istack.dev59.com/rlS4y.webp)
不要将
double.Epsilon
(或
ε
)误认为是指数字母
e
。
double.Epsilon
在某种程度上是其微积分计算机制的表示,这可能意味着大于
0
的最小实数。
因此,只为确保我们已准备好编码,让我们回顾一下这里发生了什么:
我们有从 0
开始并以 ε * (N-1)
结束(其中 ε
或 double.Epsilon
是大于 0
的最小 double
)的 N
(N
为 2²⁴
)个 double
数字。
在某种意义上,我们创建的 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;
[FieldOffset(0)]
private double _AsDouble;
private const int N_MINUS_1 = 256 * 256 * 256 - 1;
private static readonly double RAW_RANGE_LENGTH = double.Epsilon * N_MINUS_1;
public double AsDouble {
get { return this._AsDouble / RAW_RANGE_LENGTH; }
set { this._AsDouble = value * RAW_RANGE_LENGTH; }
}
}
你会惊喜地发现,在这个
编辑之前我提出的测试仍然有效,加入新元素后信息丝毫未损失,现在双倍范围均匀延伸至
0 .. 1
。