ALGORITHM ALLEY

The Popularity Algorithm

Dean Clark

Dean is a programmer/analyst developing graphics and imaging applications and doing user-interface design. He can be reached on CompuServe at 71160,2426.


Compared to the once-standard 16-color VGA, 256-color graphics systems are a significant improvement. They give us enough range to be subtle about the colors in our designs, and we don't have to worry about falling off the edge of the world at every turn. But are 256 colors really enough? What about displaying realistic color imagery such as scanned photos and ray-traced simulations? Most of these large images contain thousands of colors.

Does this mean that we need graphics hardware capable of displaying at least several thousand colors before we can see pretty pictures on our PCs? Of course not. Practically everyone with a 256-color VGA card has seen GIF, PCX, or TIFF scanned images of incredible realism, even on 256-color Super-VGA hardware. It's unlikely that the original pictures had only 256 colors in the first place. At some point, someone picked the 256 best colors to use for those images--a process called "color quantization."

The basic problem of color quantization is straightforward: Out of the set of all possible system colors, select the N colors most representative of a particular image and then map the colors in the image to those representatives. There are several techniques commonly used to implement color quantization, one of which is called the "popularity algorithm." (For a brief discussion of other techniques, see the accompanying text box entitled, "Other Color-Quantization Methods.")

Eight-Bit Color Basics

All 256-color workstations and PCs work pretty much the same way. The graphics hardware provides a universe of colors composed of red, green, and blue primaries (RGB). Varying the proportion of individual RGB primary values results in distinct colors on the screen. If all the values are 100 percent, you see white; if all of them are 0, you see black.

The range of individual values depends on the graphics device. UNIX workstations typically support 256 shades of each primary color for a total of 16 million potential colors--each primary is 28 bits, so three of them together are (28)3=224, or 16,777,216. That's how many distinct colors the hardware is capable of, but only 256 of them can be displayed at a time.

A 256-color VGA card supports 64(26) shades of each primary color. Putting all the primaries together gives (26)3=218, or 262,144 distinct colors. This is fewer than what's available on workstations, but it's still a lot of colors.

The RGB colors available for display are stored in the graphics system's palette table, essentially an array indexed from 0 to 255. The software sets a color on the screen by specifying a palette-table index. Applications can set any palette-table location to any RGB color the system can support.

The Popularity Algorithm

The maximum number of possible distinct colors in a computer image is its width in pixels times its height in pixels; that is, there can be no more than one distinct color per image pixel. A 640x480 image, for example, can have as many as 307,200 different colors. A quantization function should map each distinct color in the original image to one of the colors in your palette table so that when the image is displayed, it looks like the real thing.

The popularity algorithm uses the most-frequently occurring colors in the image as the palette colors. Every other color is then mapped to the popular color it's closest to. The steps are:

  1. Scan the image, building a list of all colors found in the image. Keep a count of the number of occurrences of each distinct color.
  2. Sort the colors in descending order by count (ties can be broken arbitrarily) and select the top 256 colors for the palette map.
  3. Rescan the image to map image colors to palette colors. For image colors that don't exactly match a palette color, use the closest color in the palette.

Implementation Details

RGB colors are commonly thought of as a cube, where each axis of the cube corresponds to one primary component of a color. For VGA, each axis of the color cube is 64 units long, for a total of 262,144 individual color cells. For Step #1 of the algorithm, you can allocate a 3-D array of integers and use the RGB values as indexes into the array.

Unfortunately, this array would be over 1 Mbyte in size, and for most images nearly all of it would be empty. Instead of statically allocating a cube, this implementation builds one dynamically by using a sparse 3-D matrix. The matrix is built as three orthogonal linked lists, where each list represents an axis of the color cube. Each node of the sparse matrix contains two pointers--one to its neighbor and another to the next color axis. That is, a red node contains pointers to the next red node and to a list of green nodes. Each green node contains pointers to the next green node and to a list of blue nodes. The structure is something like Figure 1.

Every color is completely specified by the path to its blue component, so you store the color counts there. To store a color, each axis is scanned in turn until all three color components have been matched. If any component is not found, a new node is created. To speed the search a little, each list is kept sorted.

To find the most popular colors in the image, you need to access them in order by count. The function AddColorToList() in Example 1 maintains an array of pointers (popcolors) to the most popular nodes in the color matrix. When the first image scan is finished, popcolors will point to the 256 colors for the new color-palette table. (Example 1 is excerpted from the popular.c program, which is available electronically; see "Availability, page 3.)

Finally, the new quantized color image is built. As the original image is rescanned, the popcolors table is searched for a matching RGB value. If a color in the table exactly matches the image color, then the color-table index is displayed or saved. If you go past the 256th element, then you use the index of the color in the table closest to the image color.

Vector Basics

What do I mean by "closest" color? In a qualitative sense, you want the color in the table that is the least distinguishable from the color in the image. In quantitative terms, you want the color in the table that is the shortest straight-line distance from the actual color. Remember that all the colors are inside a 3-D cube, so the distance between two colors is d=half bracec2-c1half brace, where c1 and c2 are two points in the color cube defined by their individual R, G, and B values. In algebraic terms, d=sqrt((r2-r1)2+(g2-g1)2+(b2-b1)2), where d is always nonnegative because of the squared terms, and implying that (c2-c1)=(c1-c2). The smaller d is, the closer the two colors are; if d is 0, then the colors are the same. In practice, you eliminate the square-root calculation, since the square-root function doesn't change the ordering of the numbers (if a>b then sqrt(a)>sqrt(b)).

The Code

The program, popular.c (available electronically), reads a 24-bit image file, creates a 256-color table, then either displays the image or writes it to a PCX file. The input image file is assumed to be an ASCII file, where the first line is the image title; the second, a description; the third, the number of columns (x) and rows (y); and the fourth, a floating-point number for the maximum image-intensity value (this number is ignored in this application). What follows are rowxcolumns of RGB "vectors" of floating-point values, where 1.0 is the maximum value for any color component. I use this simple file format in my rendering applications because it's very easy to work with while debugging the hard stuff.

As each image RGB value is read, it is converted to VGA (6 bits per primary) and added to the color matrix, or its count is incremented if it's already in the matrix. The popular-colors list is then updated. When the entire image has been read, LinearizePopcolors() extracts the most popular RGB components from the popular-colors list. This is simply a convenience to help make the next scan a little easier and faster.

The program then asks whether it should display the image or write it to a file. As written, the program uses the MetaWINDOW graphics-kernel system from Metagraphics Software (Scotts Valley, CA) for displaying images. However, all you really need are graphics initialization, VGA color palette, and pixel-drawing functions, which are universal in any graphics library or even through BIOS calls. The PCX code is also straightforward.

Results and Additional Heuristics

The proof of the pudding is in the eating, and the proof of any computer graphics technique is the result on the screen. Figure 2 is a computer-generated graphic at 24-bit color resolution. The image is 512x375 and contains about 4200 distinct colors. Figure 3 shows the same image quantized to 256 colors using popular.c. By most standards, the quantized image is an acceptable approximation.

The algorithm might miss small but visually significant areas of colors if none of them are popular enough. This is much more likely to occur on workstations, where each primary color is eight bits instead of VGA's six. The result can be that no color in the quantized palette table is close enough to these areas to produce good results.

One way of addressing this is to reduce the color resolution. Instead of eight or six bits of color, use five or even four bits. The sample program does this by prompting for a color "compression factor" for each primary color. These factors are used to reduce the number of distinct primary colors on each axis. Lowering the effective color resolution allows more image colors to become clumped around a single point in the system color cube; that is, the points in the color cube get "bigger." Fewer colors fall outside the clumps, so more fringe colors are represented.

Figure 4 shows the sample image quantized with five bits of color; Figure 5 shows it with four. Note that the highlights in the spheres and light reflections in the background mirrors are becoming more distinct, but the smooth-colored red floor begins to show serious banding. This is a typical result when color resolution is lowered. Which image--6-bit, 5-bit, or 4-bit--is "best" is subjective.

Most images have far fewer than their theoretical maximum number of colors, and those colors tend to group into a relatively small number of regions of similar colors. This is called "color coherence." Consider a photograph of a child on a sunny day in the park. The main elements of the photo are the child's face and clothing, the sky, grass and trees, and clouds. The sky is likely subtle shades of blue, the clouds are mostly gray-white, the grass and trees are expanses of green, and so on. A careful scan of the photo at VGA resolution and 24 bits of color might result in 60,000 distinct colors.

Yet most of those colors are in one of a few regions; blues for the sky, greens for the grass and trees, grays for clouds, and skin tones for the child. The many distinct colors in the scan are largely the result of minute subtleties of shading.

You might be able to improve some quantized images by making sure you get a representative sample from different regions in the color cube. Let's implement a heuristic that divides the output color palette into four regions; red-dominant, green-dominant, blue-dominant, and gray-dominant. A color is dominated by its brightest component. For the gray-dominant region, no primary color is significantly brighter than the others. I'll allow 64 elements of the 256-color palette for each dominant region.

In the sample program, if the user chooses to apply the color-dominance heuristic, the initial scan distributes input colors into four popularity-ordered lists instead of just one. After scanning, the four lists are combined into a single color palette by taking the most-popular 64 colors from each region. If a region doesn't have 64 distinct colors, its leftover palette space is distributed among the others.

Just as you have to define "closest color," you also have to define "dominant." There's no hard rule. A fast test would be that a primary color is at least N greater than either of the other colors, say 15. So the rgb value (56,37,18) is red-dominant, but (12,1,2) isn't. A different rule would make the dominant color N% greater, say 15 percent. By this rule, both of the example colors are red-dominant; popular.c uses a percentage of 20 percent.

You could extend this approach by distributing the colors among the eight octants of the color cube, or even use image characteristics to drive the distribution, but the code quickly becomes complex and, it turns out, the benefits tend to diminish. Figure 6 shows the sample image with the color-dominance heuristic applied.

Conclusion

For a long time I thought I had "invented" the popularity algorithm. It turns out, however, that Paul Heckbert published a paper describing the algorithm in 1982: "Color Image Quantization for Frame Buffer Display," (ACM 1982 SIGGRAPH Proceedings, Vol. 16, No. 3). The same paper describes another quantization technique called "Median-cut" (see also "Median-Cut Color Quantization," by Anton Kruger, DDJ, September 1994).

The algorithm's running time is bounded mainly by the number of pixels in the original image and the size of the quantized color table. These values drive the two image scans, the sparse matrix insertions, and the code that maintains the popularity list. The memory costs are bounded by the number of distinct colors in the original image. In practice, popular.c processes about 2800 image pixels per second on a 486/33 and spends most of its time accessing the disk. The program could be sped up somewhat by postponing the popularity ordering (AddColorToList()) until the color matrix has been built. There's a little extra unused space in each matrix node. The program assumes there will be at least 256 distinct colors in an image.

The popularity algorithm is easy to implement and tinker with, making it well suited for experimentation. Best of all, the resulting images are generally quite good.

Other Color-Quantization Methods

The popularity algorithm isn't the only technique for mapping 24-bit imagery to 8-bit lookup-table systems. Two other common techniques are median-cut and octree quantization.

The median-cut algorithm was first published by Heckbert, in the same article as the popularity algorithm. As in the popularity algorithm, you first create a cube structure containing all the colors in the original image. The cube is then recursively subdivided along its axes such that about the same number of pixels are represented in each subdivision. When there are 256 subcubes (or some other target number), the colors within them are averaged to find the lookup-table colors.

Michael Gervautz and Werner Purgathofer published their octree quantization technique in New Trends in Computer Graphics (edited by Nadia Magnenat-Thalman and David Thalman, Springer-Verlag, 1988). This technique was later summarized in Graphics Gems (edited by Andrew S. Glassner, Academic Press, 1990). As the name implies, an octree is a tree structure in which each node has eight children. As the original image is scanned, unique colors are inserted into the octree. Inserting the 257th color (or N+1) causes the tree to be reduced by merging the two closest tree colors into a single color. This method is unique in that there are never more than the final number of colors stored in the tree.

--D.C.

Example 1: The AddColorToList function.

/****************************************************************************
** AddColorToList
** Adds a color to the list of most popular colors in the image, if necessary
*****************************************************************************/
void AddColorToList(COLOR_NODE **popcolors, int npal,
                                int *currentcount, COLOR_NODE *color)
{
    COLOR_NODE  *temp;
    int         i;
    /* If table is empty then insert this color */
    if (*currentcount < 0) {
        *currentcount = 0;
        popcolors[*currentcount] = color;
        return;
    }
    /* Search the table for the same color */
    for (i = 0; i <= *currentcount; i++) {
        if (popcolors[i] == color) break;
    }
    if (popcolors[i] == color) {
        /* Found it. Since the color is already in the table, adjust it
        ** to its proper position */
        while (i && (popcolors[i-1]->count < popcolors[i]->count)) {
            temp = popcolors[i-1];
            popcolors[i-1] = popcolors[i];
            popcolors[i] = temp;
            i-;
        }
        return;
    }
    /* This color isn't in the list.  See if it belongs there.  First of all,
    ** if the list isn't full, this color must have a count of 1 and can be
    ** simply added to the end */
    if (*currentcount < npal-1) {
        (*currentcount)++;
        popcolors[*currentcount] = color;
    }
    else {
        /* Otherwise the list is full and this color may belong in the list.
        ** Start at the low end (if the color had a high count it would
        ** already be in the list) */
        if (color->count > popcolors[npal-1]->count) {
          i = npal - 1;
          popcolors[i] = color;
          i-;
          while ((i >=0) && (popcolors[i]->count < popcolors[i+1]->count)) {
                temp = popcolors[i+1];
                popcolors[i+1] = popcolors[i];
                popcolors[i] = temp;
                i-;
          }
        }
    }
}
Figure 1: Sparse 3-D matrix structure.

Figure 2: Original 24-bit image with a 512x375 resolution and about 4200 colors.

Figure 3: Same image as Figure 2, but quantized to 256 colors using the popular.c program.

Figure 4: Same image as Figure 3, but quantized with five bits of color.

Figure 5: Same image as Figure 3, but quantized with four bits of color.

Figure 6: Sample image with color-dominance heuristic applied.


Copyright © 1995, Dr. Dobb's Journal