Device-Directed Rendering

The Main Idea

Synthetic image rendering systems can produce image descriptions that require the entire range of visible colors. Imaging hardware, however, can reproduce only a subset of these colors: the device gamut. An image can only be correctly displayed if all of its colors lie inside the gamut of the target device. The gamut varies from device to device. For example, most color CRTs can display cyans and yellows that cannot be printed, and many color printers can create blues and greens and that cannot be displayed on a CRT.

The solution to this problem is often to take the rendered picture and somehow distort it to fit the allowable colors for a given device. This approach has two drawbacks. First, typically an image is created on a CRT and tuned there, only to be printed when done. Such a process sacrifices all the colors that the printer makes available that the CRT lacks. Secondly, there is no way to adjust colors locally (say, pixel-by-pixel) and still preserve what we call the semantic integrity of the image. For example, if you change the color of pixels that look directly upon an object, you need to change the color of pixels that include a reflection of that object. But there's no way to identify or fix those pixels when all you have is colors and pixels.

Our insight was to realize that if you save the history of how each pixel was computed, then you have enough information to recalculate the image correctly using new colors for the lights and objects.

This was a joint project with Ken Fishkin, David Marimont, and Maureen Stone of Xerox PARC.

Results
DDR target DDR result
On the left we see the target image from a scene of three cylinders in a small box. We simply projected the image to the gamut to derive the target, and then repeatedly moved towards that image. Notice that the cylinders appear flat in the target, since all of their colors have been reduced to a single (clipped) color. On the right is the resulting, in-gamut image; note that there are now enough colors available for the cylinders to appear rounded.
DDR target DDR result
In this example we include reflections: a box sits in a room with a blue floor and two walls made of partly-reflecting mirrors. The main part of the ball and its reflections are in gamut, but the highlights on the ball (and the reflections of the highlights) are out of gamut. Notice how the reflections of the ball in the mirrors in the resulting version are correct; that is, the reflections have changed to match the change to the ball itself. A local mapping procedure which only adjusts pixels could not preserve this semantic information.
Details

The heart of the method is to realize that when you compute an image, you have a lot of informaiton available that is not available when you have just a rectangular array of colors. When rendering, you know just how much each light source and each object is contributing to each pixel.

To start, we need to define color vectors. A color vector represents the spectral distribution of light at a variety of wavlengths. At a minimum, each color vector is a single value (for a grayscale image), but more typically they have 3 components (one each for red, green, and blue). More sophisticated renderers can support more detailed color descriptions. We will write such vectors in bold, e.g. V.

Consider a pixel which is looking at a slightly shiny ball. The ball is lit by a light source with color L. The ball itself is coated with paint that when illuminated with perfectly uniform white light, reflects a color C. Furthermore, at the particular angle which is seen by this pixel, the ball reflects a percentage a of the incident light specularly, and a percentage b diffusely. So the specular component is just the light source color scaled by the specular reflectivity, or aL. The diffuse component is the light source color modulated by the surface, and then scaled, giving bL C. The total color P at the pixel is then P = aL + bL C. This is the basic form of all pixel expressions; most have many more terms, but they're all just sums and products of colors and scalars.

We will assume that a and b are fixed, since changing them can drastically alter the image (imagine cranking up the specular component of a person's skin). However, we will be free to change the colors L and C. Our pixel expression above is a vector expression; if the colors are stored with three RGB components, then it's actually three scalar equations. For example, the red component would be Pr = a Lr + b Lr Cr, and similarly for green and blue. The heart of DDR is to realize that we can differentiate this expression with respect to Lr, which will then tell us how much Pr changes for each little change in Lr.

In DDR, we first render the image to get a vector expression for each pixel, which we then break down into scalar expressions. We differentiate each of these expressions for each of the pixel components, so that we can quickly plug in any desired change to the colors and immediately see the resulting change in the pixel values. Now we step back and look at the colors we have at the pixels, and check to see if everything is in (or close to) gamut. If the picture is in gamut, we're done. Otherwise, we create a target in-gamut picture by simply finding the nearest in-gamut color for each out-of-gamut pixel. This is just like local gamut mapping, except we don't actually use these colors for display. Instead, we pretend that this is the picture we want, and we find the difference between each current pixel component and the target component. That is, suppose pixel (3, 55) has color components (135, 265, -5), and we want to map to a color CRT that can display only in the range [0, 255] for each component; we would want to move that pixel by (0, -10, 5).

The goal is to find some change to the colors that will match the desired changes to the pixels. Of course, we can never get all the desired pixel changes, since the target picture has been mapped without regard to the underlying scene description. But it gives us a direction to move in order to get closer to the gamut. So we take all our differentiated pixel expressions and arrange them in a matrix: one row per pixel component, one column per color component. We can write this as a matrix formula dP=J dW, where dP is the column of changes to pixel components, W is the set of current color weights, and J is the matrix that tells us for each change to the weights, what change we get in the pixels. This matrix of partial differential equations is called the Jacobian. This equation is backwards; we know how much we want the pixels to move dP, and we want to find the changes in the scene colors dW to get those pixel changes. So we invert the matrix and write dW = J* dP, where J* is the pseudo-inverse of J (this is necessary because J is typically not square).

Given these changes dW, we alter the scene colors and try again, creating a new picture. If it's not in gamut, we project it to the gamut again, creating a new set of desired pixel changes dP, and compute a new set of scene-color changes.

In our experience, this process always converges, but frankly we don't have any ideas for how to prove it. The choice of target also influences the algorithm; one could argue that rather than computing a new target after each iteration, the target should be held fixed throughout the process. We address these issues in our TOG paper, referenced below under More Info.

More Info

This algorithm is discussed in detail in our paper "Device-Directed Rendering", which appears in ACM Transactions on Graphics. Here's the citation:

Glassner, Andrew S., Kenneth P. Fishkin, David H. Marimont, and Maureen C. Stone, "Device-Directed Rendering", ACM Transactions on Graphics, Vol. 14, No. 3, January 1995, pp. 58-76