by Kevin Goldsmith

Created

October 18, 2007

When we left off in part 1, we had a filter that took a color image and then created a black and white image by using the red channel from original image for the red, green and blue channels of the output image. Going from this:

to this:

And here is the hydra filter that we left off with:

kernel FadeToHistory

{
void
evaluatePixel(in image4 src, out pixel4 dst)
{
dst = sampleNearest(src,outCoord()).rrra;
}
}

If you would like to download what we have so far, a link to the above hydra filter is here: FadeToHistory2.hydra

Now, onward and upward!

Part Four: Making it interactive

We now have a filter that takes a color image and does a simple conversion to black and white. This is nice, but we could do it in Photoshop and use that instead. Where Hydra gets cool is by allowing you to interact with the filters, automate and animate them. Let’s add a parameter to the filter that will allow us to animate the image from color to black and white. The new filter looks like this:

kernel FadeToHistory

{
parameter float crossfade;

void
evaluatePixel(in image4 src, out pixel4 dst)
{
dst = sampleNearest(src,outCoord());
float3 bw = dst.rrr;
dst.rgb = ((1.0 - crossfade) * dst.rgb) + (crossfade * bw);
}
}

There are a few changes here:

  1. parameter float crossfade; This line tells the compiler that you want a parameter to your filter that will be exposed to the application hosting your filter. In this case, a float parameter. If you run the filter above, you will see a slider in the area to the right of your image. Moving this slider changes your parameter value.
  2. dst = sampleNearest(src,outCoord()); We’ve reverted this back to what it used to be when we did new filter. We’re going to use the dst variable to store the color version of the image.
  3. float3 bw = dst.rrr; I’ve created a temporary variable to store the black-and-white version of the rgb channels from the original image. Float3 is an array of three float values. In Hydra, pixel values are stored as floats.
  4. dst.rgb = ((1.0 – crossfade) * dst.rgb) + (crossfade * bw); This is a simple crossfade calculation using the crossfade variable to determine the amount. When the crossfade parameter is 0, you get the color image. When the crossfade parameter is 1, you get the black and white image. We overwrite the red, green, and blue colors from the original image with the mix of color and black and white values based on the crossfade parameter.

Part Five: Better Black and White conversion

If you’ve tried this Hydra kernel on any image without a lot of red content in it, the results are pretty unsatisfying. Time to do a better conversion.

kernel FadeToHistory

{
parameter float crossfade;

void
evaluatePixel(in image4 src, out pixel4 dst)
{
dst = sampleNearest(src,outCoord());
float luminance = dst.r * 0.3 + dst.g * 0.59 + dst.b * 0.11;
dst.rgb = ((1.0 - crossfade) * dst.rgb) + (crossfade * float3(luminance));
}
}

There are a couple of important things here:
float luminance = dst.r * 0.3 + dst.g * 0.59 + dst.b * 0.11;

This line computes a single float value that is a combination of the red, green and blue colors from the original image. This formula is pretty similar to how color TV images are converted for old black and white TV screens. The values are based on how your eyes perceive color. If you used 0.33 for each of the multipliers, you’d actually have something that looked kind of wrong because your eyes don’t perceive blue and red in the same way you see green. For more info, check out this wikipedia article.

float3(luminance)
Here we create a new float3 variable in-line where each of the three indicies are set to the value of luminance.

Part Six: A Bit of Clean Up

For this last part of this post, I’m going to make a couple changes to clean up the code. These will simplify some things as well as show some more built in functions for Hydra.

kernel FadeToHistory

{
parameter float crossfade;
const float3 lumMult = float3(0.3, 0.59, 0.11);

void
evaluatePixel(in image4 src, out pixel4 dst)
{
dst = sampleNearest(src,outCoord());
float luminance = dot(dst.rgb, lumMult);
dst.rgb = mix(dst.rgb, float3(luminance), crossfade);
}
}

With this iteration, I’ve created a const variable:
const float3 lumMult = float3(0.3, 0.59, 0.11);
This variable contains the numbers I multiply red, green, and blue by to get the luminance value. Since these numbers do not change ever, I can put them in a const variable. This simplifies the code and allows the compiler to do some optimization because I’m telling the compiler that this value will not change.

float luminance = dot(dst.rgb, lumMult);
I’ve changed from multiplying the red, green and blue components of the input image by the luminance calculation and then adding them together to using the built-in dot function which does exactly that. Dot specifies the dot product of two vectors, which is computed by multiplying the components of the vectors and then adding them up (which is exactly what we were doing to compute the luminance value). Fore more info on dot products, this wikipedia article is extensive.

dst.rgb = mix(dst.rgb, float3(luminance), crossfade);
Since operations like the crossfade that I was doing before are pretty common in image processing, we’ve have a built-in function to do linear interpolation between two values. This is the mix function, and I use it here rather than doing the computation myself.

At the end of part 6, we have a kernel with a parameter that lets us smoothly animate from a color image to a luminance-based black and white image. To play with the filter, you can download the Hydra file: Download the Hydra file