In my last entry, I provided some code to convert from the HSB color space to the RGB color space.
Now, the time has come to make this code come to life, rather than being interesting only to those who are interested in math or color spaces!
To explore some of the possibilities of using the HSL color space (which maps more intuitively to the human perception of color), I created a class deriving from Panel, and used the background color to generate 4 different colors with the same hue, but different values for saturation and brightness (luminance), which I then used to creating an interesting looking gradient.
Custom Gradient Panel Based On a Single Color's Hue
This highlights some of the interesting possibilities of mapping color to something that humans feel more comfortable manipulating. Starting with a single color, I can now generate a gradient, and I can generate it for all possible colors. This might be useful if we wanted to create an interesting looking toolbar (shown at the top), or interesting looking headings (shown in the body) without having to specify specific colors for each endpoint of the gradient.
The top panel (the one emulating a toolbar) is based on a System Color - Active Caption. Using this technique, we are now generating a 4 color gradient based on the active caption color - in this case blue. If I switch the color scheme to the olive green one, now my color is based on olive green. Handy! However, we'll come back to this one in a second. The header panel I have labelled Group 1 is based on the color Red. The header panel I have labeled Group 2 is based on the system color Control.
Wait a minute ... isn't the Control system color grey, and not in any way electric purple? That is one thing to watch out for if you attempt to base the colors on external colors that are outside of your control. Note that we are modifying both saturation and brightness (luminance) to come up with our gradient. In the HSL color space, there is no hue for grey at all - grey is simply a color (any color you want) with a saturation of 0. Hues of 0 through 360 with a saturation of 0 and a brightness (luminance) indicating the shade of grey are all exactly identical. So, when we take a shade of grey, we can never be entirely sure which specific hue will have been used, since it does not matter in the least. As such, it is probably best to avoid using this technique and tying it to a color over which you have no control, because shades of grey break the model in this color space. You will notice the same thing in the panel I am using as a toolbar if you change your system colors to the silver color scheme: instead of a grey gradient, you end up with a red gradient!
Of course, we could take this into account and more intelligently modify saturation and lightness, rather than just hard coding explicit values as we have done here. This is left as an exercise for the reader.
With that said, let's take a look at the code:
class FourColorPanel : Panel { private Color topTopColor; private Color topBottomColor; private Color bottomTopColor; private Color bottomBottomColor; public FourColorPanel() : base() { SetBackgroundColors(BackColor.GetHue()); } public override System.Drawing.Color BackColor { get { return base.BackColor; } set { base.BackColor = value; SetBackgroundColors(value.GetHue()); } } private void SetBackgroundColors(float baseHue) { topTopColor = ColorConversions.ColorFromAhsb(255, baseHue, 0.2958f, 0.7292f); topBottomColor = ColorConversions.ColorFromAhsb(255, baseHue, 0.5875f, 0.35f); bottomTopColor = ColorConversions.ColorFromAhsb(255, baseHue, 0.7458f, 0.2f); bottomBottomColor = ColorConversions.ColorFromAhsb(255, baseHue, 0.6f, 0.4042f); } protected override void OnPaint(PaintEventArgs e) { Rectangle topRect = new Rectangle(0, 0, ClientRectangle.Width, ClientRectangle.Height / 2); Rectangle bottomRect = new Rectangle(0, topRect.Height, ClientRectangle.Width, ClientRectangle.Height - topRect.Height - 1); using (Brush topBrush = new LinearGradientBrush(topRect, topTopColor, topBottomColor, LinearGradientMode.Vertical)) { e.Graphics.FillRectangle(topBrush, topRect); } using (Brush bottomBrush = new LinearGradientBrush(bottomRect, bottomTopColor, bottomBottomColor, LinearGradientMode.Vertical)) { e.Graphics.FillRectangle(bottomBrush, bottomRect); } } }
Hopefully, this technique can open up new doors for creating interesting color combinations based on a single hue, rather than requiring that every single color be explicitly specified.
I want to digress a bit from the controls we have been drawing and measuring to talk a bit about color. In some of the examples we have been building, we have been trying to make it possible to set the color just one time rather than explicitly specifying background colors and highlight colors. Consequently, we have been doing our highlights using white, which ensures that they work equally well with any color the consuming application selects. Unless, of course, the consuming application select white, in which case it's not so great.
In the last control we were building, we were explicitly hard coding two colors, and using a radial gradient fill using these two colors. Now, we are putting the impetus on the developer of the consuming application to select two colors that look good together, rather than a single one - and both of these must look good with white as the highlight color. We are making the job more and more complex for the developer consuming our controls.
It would be easier if we could programatically discover related colors, wouldn't it? That way, the developer of the consuming application could input a single color, and we could intuit related colors and use them. One approach is to use the HSB color space - Hue, Saturation, and Brightness. Using this approach, we can start with one color, and then vary these properties, which seem more intuitive. What does it mean to add 20 to the blue value of a given color? We have to think about that one for a bit. What does it mean to add 20 to the brightness of some color? We can picture that much more easily.
Starting with the same Hue, we could vary the brightness - creating a highlight that fits with the starting color. If we have a more advanced understanding of complementary colors, we could discover a complementary hue around the 360 degree Hue value space. The space is more easy to navigate. Plus, if you have used the Color Picker dialog, you may have already noticed that HSB (here referred to as Hue, Saturation, Lumens) is something that is widely used throughout Windows applications.
Color Picker dialog box
We used a special case of this scenario while creating Gel Buttons earlier, converting to grayscale by applying the brightness value to the R, G, and B channels. Now, we just need to finish our work to perform this conversion in the general case.
Of course, as good developers, our first instinct is to let somebody else do that work, so we can focus our energy on solving unique problems that do not yet have a known solution. Searching around a bit, you can find that somebody has a class submitted to The Code Project to do exactly that: Use both RGB and HSB color schemas in your .NET application using HSBColor class. How convenient! Unfortunately, if you use the algorithm provided by this class, you will notice that if you take an existing Color structure, retrive the HSB values using GetHue(), GetSaturation(), and GetBrightness(), the Color that is returned is not the color that you began with. That's certainly not a good thing.
GetHue()
GetSaturation()
GetBrightness()
Poking around a bit more, I discovered another site called Color Conversion Algorithms at the Rochester Institute of Technology. However, the algorithm it presents had similar problems - the HSB model it used apparently differs from the HSB model used by the .NET Framework.
Since I think it is important to not overload the terms Hue, Saturation, and Brightness within a platform, it appeared as if I was going to need to create the implementation on my own. While I could have simply provided both the mechanism to convert both to and from HSB from the standard RGB values, ensuring the symmetry of the altorighms uses, I want somebody to be able to pass in the results of a call to GetHue(), GetSaturation(), and GetBrightness() and correctly anticipate the results. So, it's time to figure out how the algorithms differ.
Taking the values one by one, it is immediately apparent that Brightness is computed differently. In the algorithms used by the above implementations, the Brightness is always set to the value of the RGB color with the highest value. However, this is obviously not true of the implementation in the .NET Framework. If you plug in a few examples, you can fairly quickly determine that the Brightness is, instead, computed as (Min(R,G,B) + Max(R,G,B)) / 2. To me, this actually made more sense than simply taking the maximum value. If, for example, you used the Brighness value to convert to grayscale, the above algorithms will make pure red, pure green, and pure blue all indistinguisable from pure white. With the .NET Framework implementation, it would be half as bright, and clearly distinguishable.
Saturation turns out to have a similarly more sophisticated algorithm. Hue, however, turns out to be identical, which is completely logical.
Once we have computed these formula, it is simply a matter of performing some high school-level algebra to compute values going in the other direction. The result is the following algorithm, expressed in a C# implementation:
public static Color ColorFromAhsb(int a, float h, float s, float b) { if (0 > a || 255 < a) { throw new ArgumentOutOfRangeException("a", a, Properties.Resources.InvalidAlpha); } if (0f > h || 360f < h) { throw new ArgumentOutOfRangeException("h", h, Properties.Resources.InvalidHue); } if (0f > s || 1f < s) { throw new ArgumentOutOfRangeException("s", s, Properties.Resources.InvalidSaturation); } if (0f > b || 1f < b) { throw new ArgumentOutOfRangeException("b", b, Properties.Resources.InvalidBrightness); } if (0 == s) { return Color.FromArgb(a, Convert.ToInt32(b * 255), Convert.ToInt32(b * 255), Convert.ToInt32(b * 255)); } float fMax, fMid, fMin; int iSextant, iMax, iMid, iMin; if (0.5 < b) { fMax = b - (b * s) + s; fMin = b + (b * s) - s; } else { fMax = b + (b * s); fMin = b - (b * s); } iSextant = (int)Math.Floor(h / 60f); if (300f <= h) { h -= 360f; } h /= 60f; h -= 2f * (float)Math.Floor(((iSextant + 1f) % 6f) / 2f); if (0 == iSextant % 2) { fMid = h * (fMax - fMin) + fMin; } else { fMid = fMin - h * (fMax - fMin); } iMax = Convert.ToInt32(fMax * 255); iMid = Convert.ToInt32(fMid * 255); iMin = Convert.ToInt32(fMin * 255); switch (iSextant) { case 1: return Color.FromArgb(a, iMid, iMax, iMin); case 2: return Color.FromArgb(a, iMin, iMax, iMid); case 3: return Color.FromArgb(a, iMin, iMid, iMax); case 4: return Color.FromArgb(a, iMid, iMin, iMax); case 5: return Color.FromArgb(a, iMax, iMin, iMid); default: return Color.FromArgb(a, iMax, iMid, iMin); } }
Of course, we had to do quite a bit of mathematical manipulation to arrive at this implementation. I am not sure if you are willing to rely on my mathematical acumen, but I know that I certainly am not! This calls for unit testing.
This is actually an interesting bit of code to implement unit tests for. The theoretical number of combinations of HSB values you could provide is infinite. (I say theoretical because the implementation of the float data type is not infinitely precise.) So, where do we begin?
One obvious starting point is to begin with each possible permutation of RGB colors. If we pass in the HSB values computed by the .NET Framework to create a new color, do we end up with the same color? We can write this test fairly easily:
public void TestAllArgbValues() { for (int a = 0; a <= 255; a++) { for (int r = 0; r <= 255; r++) { for (int g = 0; g <= 255; g++) { for (int b = 0; b <= 255; b++) { Color startColor = Color.FromArgb(a, r, g, b); Color endColor = ColorConversions.ColorFromAhsb(startColor.A, startColor.GetHue(), startColor.GetSaturation(), startColor.GetBrightness()); Assert.AreEqual(startColor.A, endColor.A, string.Format("Alpha does not match for {0},{1},{2},{3}", a, r, g, b)); Assert.AreEqual(startColor.R, endColor.R, string.Format("Red does not match for {0},{1},{2},{3}", a, r, g, b)); Assert.AreEqual(startColor.G, endColor.G, string.Format("Green does not match for {0},{1},{2},{3}", a, r, g, b)); Assert.AreEqual(startColor.B, endColor.B, string.Format("Blue does not match for {0},{1},{2},{3}", a, r, g, b)); } } } } }
However, it turns out that this is not such a great unit test after all. Can you spot the problem? If you said, "For the love of code, that unit test has 4,294,967,296 permutations!" you are correct. I started that unit test at 9:00 at night. The next morning, it was still running. It was only when I got home from work that day that it had completed. Fortunately, it had passed! However, we certainly don't want to spend a day running a unit test every single time, so I marked that test with the IgnoreAttribute and tried again.
IgnoreAttribute
What is a good subset of every possible permutation of RGB colors? How could you select from them? The route I took was to select all KnownColor members, which should be fairly representative, not to mention disproportionally representing the colors we are likely to receive from a caller. So, the unit test I came up with for this was:
KnownColor
public void TestKnownColors() { Array colorsArray = Enum.GetValues(typeof(KnownColor)); KnownColor[] allColors = new KnownColor[colorsArray.Length]; Array.Copy(colorsArray, allColors, colorsArray.Length); for (int i = 0; i < allColors.Length; i++) { Color startColor = Color.FromName(allColors[i].ToString()); Color endColor = ColorConversions.ColorFromAhsb(startColor.A, startColor.GetHue(), startColor.GetSaturation(), startColor.GetBrightness()); Assert.AreEqual(startColor.A, endColor.A, "Alpha does not match for color " + allColors[i].ToString()); Assert.AreEqual(startColor.R, endColor.R, "Red does not match for color " + allColors[i].ToString()); Assert.AreEqual(startColor.G, endColor.G, "Green does not match for color " + allColors[i].ToString()); Assert.AreEqual(startColor.B, endColor.B, "Blue does not match for color " + allColors[i].ToString()); } }
And, not surprisingly, this unit test passed as well. Are we done? Well, it's possible that we could pass in HSB values that do not directly correspond to an RGB color (with is much more finite integer values). How do we know that these would be converted correctly?
In order to test these permutations, we would have to come up with an operant definition for what the best Color approximation of an HSB value would be. We could potentially define this as the Color structure who's HSB values differed from the input HSB values by the smallest amount. However, then we would need to iterate over the same huge number of possible colors for each initial HSB value. Given that we have a theoretically infinite number of HSB values that do not directly correspond to a Color structure, suddently we have infinity times a huge number, with each of the infinite iterations requiring approximately a day to run. In the end, I decided that I was confident enough in my math to forego this exercise.
In the end, we have created some useful new functionality, which is consistent with the computation of HSB values that the platform provides. Later, we will use this ability to start to do some interesting things.
We have been exploring the technique of pre-rasterizing vector images, and the last time around we took a look at a particular example where pre-rendering our vector image performed better than real-time rendering when the image is small, but that the pre-rasterized version began to take much longer as the size of the controls go larger.
One thing to take note of, however, is that we are pre-rasterizing to an image of the exact size and shape as our final output will be. However, we can not safely make the assumption that this will always be the case, so it's worth a short digression to investigate what might happen if we were to pre-rasterize to a different size, and then allow the framework to scale our image on our behalf.
Why is this important? This is a technique that I know that people use. I have heard many people discussing the technique of creating a bitmap at the largest possible size, and then scaling it to fit the screen. (Most recently, I heard this in a discussion of rendering images for the .NET Compact Framework, in order to fit the varying screen sizes offered by any of the devices that might be using that software.) There is a performance cost involved; pre-rendering to the largest possible size and then scaling down will reduce performance. Obviously, there are more computations involved than a simple memory move. But what is this performance cost?
To investigate this, I began with the same house image that I used for my last entry, and rasterized it. For one test run, I created a Bitmap object whose width and height were equal to 1/2 the width and height of the client rectangle. For another test run, I created a Bitmap object whose width and height were equal to 2 times the width and height of the client rectangle. In both cases, I rendered to the full client rectangle. Finally, as a control, I rendered to a Bitmap object of the target size. I ran 100 iterations at each of the various sizes of the image. What were the results?
Rendering Performance of Scaled Bitmaps: Size of Bitmap (as a proportion of the original) vs. Rendering Speed (in ms)
In both cases, the performance was worse. While the performance penalty decreased slightly over time, the change was not dramatic. The image that we scaled up from 1/2 the original size took an average of 1.46 times longer to render at each of the sizes measured. (It also looked pixelated - another shortcoming.) The image that we scaled down (the option more people would be likely to choose, since the image ends up looking much better) required an average of 3.71 times as long to render at each of the measured sizes! As the image size grows larger, this can become very significant.
Of course, this is not the end of the story. These measurements assume the default interpolation mode. What happens if we change the interpolation mode on the Graphics object we are using? In every situation, scaling a bitmap from 2 times the size of the client rectangle to the size of the client rectangle reduces performance, but the result vary based on the interpolation mode. Let's take a glance at the results:
As you use more and more complex interpolation methods, it takes more and more time to render. (I must admit that I do not understand why an interpolation mode labeled High Quality Bicubic would outperform an interpolation mode simply labeled Bicubic by a factor of 3 to 1, but those are the results I am seeing. I ran them both several times just to make sure.)
In any case, if you are hunting down ways to optimize rendering performance, eliminating resizes can potentially provide you with a drastic performance bump, especially when you are scaling down and image quality is critical.