如何在 C# 中使用枚举和位移位制作标志

在考虑要写的帖子时,从某个电视节目中想到了标题“Fun with Flags” ,我想知道如何将其与编程联系起来。C# 中有通过属性的枚举标志Flags,所以也许这是我可以写的。也就是说,我想做一些比一些关于使用枚举的单调帖子更有创意的事情,即使最终结果并不实用。

相反,我决定用枚举在 C#中制作真正的52357729848标志 – 将数字变成标志:

德国国旗

我给自己提出了一些要求:

  • 我需要能够生成多个标志
  • 我想通过枚举将有关标志的所有内容编码为单个值

也有技术限制,因为我无法在枚举中存储大量数据,需要做出妥协。枚举可以由几种不同的类型支持,但我选择了long这样我可以获得完整的 64 位数据来使用。

编码值

我最初的想法是选择最简单的标志形式 – 带有条纹的简单标志。如果我将一个典型的标志分成 9 段,也许我可以存储 9 种颜色,这样就可以绘制水平和垂直条纹。这似乎是当时最直接的方法(我后来意识到,如果我存储“形状”数据而不是更多的标志种类可能会更好,但是哦,好吧)。

问题是,以 64 位存储 9 个段非常困难,每段大约 7.11 位,使颜色数据非常有限。我想要 3 个颜色通道,因此每种颜色只给我 2 位,种类不多。然后每段有 6 位给我留下了 10 位,我并没有太多用处。最初,我尝试使用这些位来帮助扩展颜色范围,充当特定通道的倍增器。但最后,它并没有太大用处,所以我把它剪掉了。

这是我结束的数据结构:

[PPPPPPPPPP]
[RRGGBB][RRGGBB][RRGGBB]
[RRGGBB][RRGGBB][RRGGBB]
[RRGGBB][RRGGBB][RRGGBB]

P = Padding
R = Red Intensity
G = Green Intensity
B = Blue Intensity

我的主要数据只需要 54 位,所以我的数据结构在前面有 10 位的填充。将填充放在前面可以使生成的数字更小。

我将使用ImageSharp将此值转换为实际图像。因为我有 9 个片段,所以将图像视为 3×3 像素正方形并让 ImageSharp 为我调整大小似乎是最好的主意。虽然数据的像素格式是 RGB24,所以我需要弄清楚如何将我的颜色从每通道 2 位放大到 8 位。

对于 8 位,我可以拥有的最大值是单通道 255。任何通道的全色强度都是 3,所以我决定简单地将最大值除以全色强度,留下 85 的幻数来缩放我的值。

这就是格式的具体细节,现在只是为了让它在代码中工作。

有点狡猾

知道我可以存储数据是一回事,真正让它工作是另一回事。我不经常使用按位和移位运算符,但为此,它将大量使用它们。

首先,我们需要一个颜色强度值的枚举:

public enum Intensity : byte
{
	None = 0,
	OneThird = 1,
	TwoThirds = 2,
	Max = 3
}

此处的特定值很重要,因为它们以二进制表示。

00000000 // Intensity.None
00000001 // Intensity.OneThird
00000010 // Intensity.TwoThirds
00000011 // Intensity.Max

因为这些值代表单个通道的强度,所以我们需要将它们中的 3 个组合在一起以形成我们的全彩。我们通过使用位移和按位或运算将它们组合起来,以创建我们的 6 位颜色值。

public enum Colour : long
{
	Black = 0,
	Red = Intensity.Max << 4,
	Green = Intensity.Max << 2,
	Blue = Intensity.Max,
	White = Red | Green | Blue
}

当我们在long这里使用 a 时(帮助我们进行后面的位移操作),我们设置的值适合 6 位。将颜色视为二进制字节,数据的移位和 OR-ing 看起来有点像这样:

00110000 // Red = Intensity.Max << 4
00001100 // Green = Intensity.Max << 2
00000011 // Blue = Intensity.Max
00111111 // White = Red | Green | Blue

为不同的颜色通道使用不同的强度值,我们也可以创建新的颜色。

00110000 // Red = Intensity.Max << 4
00001000 // Green = Intensity.TwoThirds << 2
00000000 // Blue = Intensity.None
========
00111000 // Yellow = (Intensity.Max << 4) | (Intensity.TwoThirds << 2)

要创建几种不同类型的标志,我们需要更多颜色…

public enum Colour : long
{
	Black = 0,
	Red = Intensity.Max << 4,
	Green = Intensity.Max << 2,
	Blue = Intensity.Max,
	White = Red | Green | Blue,
	Orange = (Intensity.Max << 4) |
		(Intensity.OneThird << 2),
	Yellow = (Intensity.Max << 4) |
		(Intensity.TwoThirds << 2),
	MediumGreen = Intensity.TwoThirds << 2,
	LightBlue = (Intensity.TwoThirds << 2) |
		Intensity.Max,
	DarkBlue = Intensity.OneThird
}

识别颜色值的正确组合相对简单——我在Paint.NET中使用了 RGB 颜色选择器,并选择了三分之一的不同颜色通道。就像如果我有三分之二的红色和三分之一的绿色,我大概会有橙色。

颜色选择器显示三分之二的红色和三分之一的绿色

所以现在我们有了颜色,我们需要对标志的最终值进行编码。在组合颜色通道的类似方法中,我们需要通过移位和 OR-ing 组合 9 个段的颜色。

public enum CountryFlags : long
{
	Germany = Colour.Black << 48 | Colour.Black << 42 | Colour.Black << 36 |
		Colour.Red << 30 | Colour.Red << 24 | Colour.Red << 18 |
		Colour.Yellow << 12 | Colour.Yellow << 6 | Colour.Yellow
}

虽然Colour.Black确实进行了编码,0因此实际上并不需要前 3 个值,但它仍然更容易将其视为所有需要颜色设置的 9 个不同的段。

在二进制中,对我们的德国国旗进行编码的操作如下所示:

        00000000 // Black, shifted by 48-bits
              00000000
                    00000000
                          00110000 // Red, shifted by 30-bits
                                00110000
                                      00110000
                                            00111000 // Yellow, shifted by 12-bits
                                                  00111000
                                                        00111000
================================================================
0000000000000000000000000000110000110000110000111000111000111000

作为一个小数,那将是52357729848. 但这只是工作的一半,我们将标志数据作为数字,但我们还需要将其解码为图像。

生成图像

那么我们如何拍摄52357729848并将其转化为图像呢?我们使用更多的位移,现在对我们的数据进行与运算来获得每种颜色。此外,我们将反向读取数据。

var blueComponent = (byte)(flagData & 3) * 85;

这里的值flagDatalong我们生成的数字。

要获得蓝色分量,我们不需要移位,但我们需要执行数据的逻辑与。我们只需要数字的最后两位——如果我们只是将数字转换为一个字节,我们将得到最后 8 位。

0000000000000000000000000000110000110000110000111000111000111000
                                    // We only want this part ^^

通过这样做flagData & 3,我们只得到完整值的最后 2 位。为了得到下一个组件,我们做同样的事情,但现在对一个位移值,所以最后 2 位是我们想要的颜色。

var greenComponent = (byte)((flagData >> 2) & 3) * 85;
var redComponent = (byte)((flagData >> 4) & 3) * 85;

现在我们有了旗帜右下角的 3 个颜色通道。提醒一下,85x 乘数用于调整颜色以适应 RGB24 像素格式的完整 8 位。实际上,现在只需将代码包装在一些循环中以将其设置为图像。

using var image = new Image<Rgb24>(3, 3);
for (var y = 2; y >= 0; --y)
{
    for (var x = 2; x >= 0; --x)
    {
        var pixel = image[x, y];
        var blueComponent = (byte)((flagData >> 0) & 3) * 85;
        pixel.B = (byte)blueComponent;
        var greenComponent = (byte)((flagData >> 2) & 3) * 85;
        pixel.G = (byte)greenComponent;
        var redComponent = (byte)((flagData >> 4) & 3) * 85;
        pixel.R = (byte)redComponent;
        flagData >>= 6;
        image[x, y] = pixel;
    }
}

在我们最内层的循环中,我们还将我们的位移动flagData超过 6 位,因此我们在下一个段中进行下一次迭代。这段代码虽然只会给我们留下一个看起来不正确的 3×3 标志,所以用更多的代码,我们可以让它更大,更像标志。

image.Mutate(x => x.Resize(400, 240, new NearestNeighborResampler()));

这里NearestNeighborResampler很重要——它允许我们在这里放大我们特定的“块状”图像,而不会扭曲或模糊它。

基本上就是这样 – 我们可以获取一堆枚举并编码一个值,然后获取该值并将其解码为图像。我已经设置了一个运行此代码的 Azure 函数来显示它的工作原理以及我生成的一些标志:

https://funwithflags.turnerj.com/api/flag/generate.png?v=ENCODED_VALUE

德国

价值:52357729848

Germany = Colour.Black << 48 | Colour.Black << 42 | Colour.Black << 36 |
	Colour.Red << 30 | Colour.Red << 24 | Colour.Red << 18 |
	Colour.Yellow << 12 | Colour.Yellow << 6 | Colour.Yellow,

意大利

价值:2532184938287088

Italy = Colour.MediumGreen << 48 | Colour.White << 42 | Colour.Red << 36 |
	Colour.MediumGreen << 30 | Colour.White << 24 | Colour.Red << 18 |
	Colour.MediumGreen << 12 | Colour.White << 6 | Colour.Red,

法国

价值:561852585091056

France = Colour.DarkBlue << 48 | Colour.White << 42 | Colour.Red << 36 |
	Colour.DarkBlue << 30 | Colour.White << 24 | Colour.Red << 18 |
	Colour.DarkBlue << 12 | Colour.White << 6 | Colour.Red,

爱尔兰

值:2532459817242612

Ireland = Colour.MediumGreen << 48 | Colour.White << 42 | Colour.Orange << 36 |
	Colour.MediumGreen << 30 | Colour.White << 24 | Colour.Orange << 18 |
	Colour.MediumGreen << 12 | Colour.White << 6 | Colour.Orange,

卢森堡

价值:13725272368788171

Luxembourg = Colour.Red << 48 | Colour.Red << 42 | Colour.Red << 36 |
	Colour.White << 30 | Colour.White << 24 | Colour.White << 18 |
	Colour.LightBlue << 12 | Colour.LightBlue << 6 | Colour.LightBlue,

最后的想法

一开始我给自己设定的一个奇怪的挑战变成了一种有趣而有趣的学习体验。我很少搞乱位移和按位运算。虽然我可能仍然不需要经常这样做,但我喜欢我现在对它们了解得更多。

如果我要再次处理这个问题,我可能会考虑编码形状而不是颜色像素。这样我就可以编码更多种类的旗帜,比如瑞典、日本甚至韩国的旗帜。

如果你喜欢这个…

如果你喜欢这有点奇怪和有趣的性质,你可能会喜欢我对 Levenshtein Distance 和各种优化的深入研究。