在C#中将双精度值转换为RGB颜色

3
我想将一个介于0和1之间的double值转换为RGB颜色。在下面的代码中,您可以看到我正在尝试做什么,但我感觉这个算法有问题。我没有得到所有的颜色。也许当我从double转换为int时会丢失信息,或者我不确定......但请看一下,如果您有任何建议或任何其他方法(经过验证),请告诉我:
    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;
    }

这里是最能表达我所尝试做的事情的图片。

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

[更新]

这是我做的,看起来是一个很好的解决方案。请看一下并告诉我这是否更好(也许那些对颜色空间有更好认识的人可以给出反馈)。

我使用了一个从这里获取的HSVtoRGB转换算法:http://www.splinter.com.au/converting-hsv-to-rgb-colour-using-c/。 知道我的值在[0,1]区间内,我将这个区间扩展到[0,360],以便使用该算法将HSV转换为RGB。在我的情况下,我使用s和v等于1。以下是代码,以便更好地解释。

        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;
        }

2
RGB颜色有3个独立的轴,而double只有1个。你想要通过颜色立方体的哪条路径呢? - AShelly
知道如果值X在0.5到1之间,它可以是任何颜色,其中蓝色等于0。否则,它将是任何颜色,其中红色等于0。 - Drill
知道如果 X 的值在 0.5 到 1 之间,它可以是任何蓝色,等于 0。否则,它将是任何红色,等于 0。这并不容易理解。尝试更好地阐述,也许然后您就可以编写正确的代码。 - Alex F
3
你的代码无法得到白色这种颜色,因为要么蓝色是0,要么红色是0。正如AShelly所说,我们至少需要知道一些预期行为的边界。 - Thomas Weller
1
另外,您在图纸上提供的公式似乎倾向于绿色色调,这是您的意图吗? - decPL
显示剩余7条评论
2个回答

12
在您的评论中,您说:“我的意图不是包含所有颜色,我不想偏袒任何一种颜色。我只想知道将双精度值转换为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;
}

现在,假设您希望以这样的方式编排编译器和即将运行的程序,使得RGB字段(每个字段占用1字节)连续布局,而AsDouble字段在其前三个字节中重叠,并继续使用其自己的独有的5个字节。如何实现这一点?
您可以使用上述属性来指定:
1. 您正在控制struct的布局(请小心,伴随着强大的力量而来的是巨大的责任) 2. RGBstruct的第0、1和2个字节开始(因为我们知道byte占用1字节),AsDouble也从struct的第0个字节开始。

这些属性可以在mscorlib.dll中的System.Runtime.InteropServices名称空间下找到,您可以在StructLayoutFieldOffset中了解它们。

因此,您可以像这样实现所有内容:

[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

最好的结束方式莫过于使用几个扩展方法:

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

不要将 double.Epsilon(或 ε)误认为是指数字母 edouble.Epsilon 在某种程度上是其微积分计算机制的表示,这可能意味着大于 0 的最小实数。

因此,只为确保我们已准备好编码,让我们回顾一下这里发生了什么:

我们有从 0 开始并以 ε * (N-1) 结束(其中 εdouble.Epsilon 是大于 0 的最小 double )的 NN2²⁴)个 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呢?因为它更快,不涉及任何数学运算,并且我们能够根据RGB输入随机访问任何一个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; }
    }

}

你会惊喜地发现,在这个编辑之前我提出的测试仍然有效,加入新元素后信息丝毫未损失,现在双倍范围均匀延伸至0 .. 1

尽管我本来想要的是另外一些东西(我无法在解释中清楚地表达),但是有了这些出色的解释,我不得不接受这个答案。你的答案是正确的,因为我的问题可能不够清晰 :S。非常感谢您的回答。我从中学到了很多东西,以后会用到这些知识。 - Drill
使用SOM时,接近的双精度值(例如0.001和0.002)之间必须存在关系,并且在表示节点时(每个节点都有自己的双精度值),这两个接近值之间的关系也应该被映射到颜色上...但是使用您的代码,我无法实现它。我知道有一种方法可以实现它,因为其他人在我之前已经做到了,我正在研究如何做到这一点,但最好的结果仍然是使用H = double value,S = 1和V = 1的HSVtoRGB算法。 - Drill
使用从0到256的双精度值(0、1、2、3、4、5...256),我从Matlab得到了这个结果:https://www.dropbox.com/s/d6dnbyfzgookjl1/Screenshot%202013-11-24%2004.39.59.png ...而使用您的代码,我得到了这个结果...https://www.dropbox.com/s/nf684xeialxn52g/Screenshot%202013-11-24%2004.54.57.png - Drill
链接已经修复。非常感谢您的阅读。祝您驾车愉快 :) - Drill
让我们在聊天中继续这个讨论 - Eduard Dumitru
显示剩余8条评论

1
如上面评论中所提到的,您绘制的公式不满足均匀覆盖整个颜色范围的条件。我认为这应该有效(远非唯一的解决方案):
*编辑:修正了公式,以前没有生成所有可能的颜色*
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 被用来解决边界场景,如 X -> 1D


我要试一下。谢谢。 - Drill
不幸的是,我已经尝试过这个方法,但在节点地图中仍然没有显示所有颜色(我知道从双值中应该有一些其他颜色,除了使用此公式显示的颜色之外:S)。在Matlab中的类似实现中,我看到有比我的应用程序更多的颜色,但我不知道他们是如何做到的... - Drill
请忽略之前的版本,因为我睡眠不足。现在它应该可以正确运行了。 - decPL
这个“不工作”具体指什么?你说有一些颜色没有被表示出来?你能确定它们的RGB值吗?另外,你确定这些数字没有某种重要的特定分布吗? - decPL
如果你只有256个数字,怎么能期望显示所有可能的颜色值呢?那么如果是3600呢?总共有1670万种颜色。我提供的代码将对0到1之间的任何双精度选择正确工作;如果你限制自己在N/256或N/3600范围内,它只会显示一些颜色。再次强调 - 你是否有关于你的数字代表什么以及可能的值的更多细节? - decPL
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接