Back to the Drawing Board
ComponentPtr is headed for the dustbin of history. While it did serve the needs for which I designed it, it wound up being much too inefficient and inflexible. For that matter, the ColorFormat class needs to be redesigned as well.
C++ allows developers to build all-encompassing abstract constructs. However, these abstract designs can sometimes be too far removed from the problem to be solved. Instead of helping to make the code clean and clear, it obfuscates and gets in the way of the proper solution.
As a case in point, let’s look at my ComponentPtr class. ComponentPtr was designed to mask out the different color component data types that we might encounter. To recap, color components, such as red, green and blue in the RGB color space used for color monitors, can be quantified using a range of values. Usually, unsigned 8-bit integers are used, resulting in a range of values from 0 to 255. In a 3-component color space, such as RGB, with each component having 256 possible values, the resulting color gamut is 16,777,216 total colors. Surprisingly, while 16 million colors is more distinct colors than the human eye can recognize, quite a few of the colors in that trinary space are indistinguishable to the human eye. Therefore, the 16 million colors covered by using 8-bit integers for each component do not encompass all the colors that the human eye can recognize. Most of the time, that’s fine, as the closest 8-bit representations are a decent enough approximation. However, to increase the gamut of available colors, we can use unsigned 16-bit integers, with each component having 65,536 possible values. This creates a color gamut of 281,474,976,710,656 distinct colors and easily defines all possible recognizable colors.
Some image manipulations require non-integral, or floating point, math. For instance, there are many mathematical functions that are used the measure the “lightness” of a color. One common formula, used by the National Television Standards Committee, or NTSC (or, jokingly, Never The Same Color), calculates luma as .299 x Red + .587 x Green + .114 x Blue. (See aside below.) In order to reduce rounding errors, there are times when we want to portray color component as floating point values in the range of 0.0 to 1.0, or 0% to 100% saturation. Due to the granularity of floating point operations, there are much, much more discrete values between the minimum and maximum color component saturation. This granularity allows us to minimize rounding errors when numerous floating point operations are done on colors in an image.
So, that leaves us with three data types to represent color components: uint8, uint16 and float. ComponentPtr was designed as a “smart pointer” defined as follows:
uint16 type;
union {
uint8* c8;
uint16* c16;
float* cr;
} ptr;
where type tells us which of the three pointer types is the one we need to use and the union allows us to overlay the individual pointer types onto one memory location.
This construct was great for defining color palette data tables and worked, somewhat, as a pixel data buffer within the Image class. Where it failed within the Image class was the assumption that each uint8 component value would only hold one color component. Let me explain:
Some images, such as black & white imagery, do not require 256 discrete values for each color component. In the case of black & white imagery, meaning true monochrome black & white and not including the gray tones in “black & white” photography, the single gray component would only need 1 bit to define the colors in the gamut. The pixel is either black or white, on or off, 0 or 1. That’s it. Some images only need 2 bits (0 to 3) or 4 bits (0 to 15) of “color depth” available to them. Not all imagery is composed of photographs. Some are cartoonish images that can either make do with a palette or can use < 8 bits of color depth directly in the pixel data buffer. This breaks the current designed utility of ComponentPtr, particularly in the area of Image pixel data buffer manipulations. How?
Let’s assume an image has a color depth of 4 bits. If we hold to the assumption that each value in a buffer of color components, as pointed to by ComponentPtr, contains data for one pixel, then only 4 bits of each 8-bit value are being used. That means that 50% of the required memory to hold the images is being wasted. (It’s worse for lower color depths.) To improve memory usage, we can “pack” multiple pixel values together. Each 8-bit value would then contain color component values for 2 4-bit pixels, 4 2-bit pixels or 8 1-bit pixels. By doing this, our memory utilization is back up to 100%. However, we’ve now added some complexity to the code. It is no longer to test if two pixel’s values are equal by comparing the values pointed to by ComponentPtr “a” and ComponentPtr “b”. Checking if a == b won’t cut it now. Furthermore, if we’re going to have to write code to “mask” out unwanted data from within individual 8-bit integers, then perhaps using 8-bit integers for packed pixel data isn’t the best course of action. After all, most computers now are 32-bit computers, able to operate on data 32 bits at a time. 64-bit processors are also being produced at the time of this writing. Why limit our algorithms to 8-bit processing chunks when our processors can easily handle 32 or 64 bits at a time?
We could get around this by adding uint32* and uint64* pointer types to our ComponentPtr class, but why bother? We’ve already broken the basic premise behind ComponentPtr. Not only was the “type” field going to tell the user which pointer was valid, but it was going to define the color depth of the color components pointed to. Bit depth was going to equal color depth. If we can now have 32-bit pointers pointing to 4-bit data, we’ve broken the bit-depth == color-depth premise behind the smart pointer. ComponentPtr, designed to easily point to a variety of color information, cannot be used easily anymore.
This is not to say that it is worthless. Color component information in a color palette vs. an image very rarely, if ever, have a color depth less than 8 bits. Therefore, we could migrate the abstract ComponentPtr class to a more specific PaletteComponentPtr class and create a derived ImagePixelPtr class that would allow the types of multiple pixels per processing unit capability that the Image class would require.
Hmmm…. maybe it’s not going to the dustbin after all.
Aside:
Transmitting color television signals across a country that was filled with “black & white” or monochrome television sets was a difficult proposition. The NTSC was set up in the US to define a signal encoding method that would allow monochrome sets to view color television broadcasts in order to avoid forcing American consumers to buy new, and more expensive, television sets. A French telecommunication engineer named Georges Valensi patented a method in 1938 to allow monochrome sets to receive color television broadcasts. Instead of transmitting RGB signals, the RGB signals are converted to a luminance/chrominance color space, where luminance is the “brightness” of the color and the two chrominance values define the color hue. The luminance value could then be read on monochrome sets while the embedded color subsignals could be safely ignored. Color sets, of course, would include the circuitry to handle and decode the color subsignal.