Friday, May 11, 2012

Any color "sepia" using Adobe PixelBender

I wanted to entitle this as "Personally proud moments in programming", but I didn't think that I would get any traffic on it. But this is one of my most proud discoveries.

About 4 years ago, I was on a project where the requirement was to custom tint photographs. The client wanted not only sepia tone, but many different colors, like yellow or green.

Many of the pathways that I tried resulted in images that would look ok only if they didn't have the target color, but if they had green, for example, they would blow out and be just awful looking.

Now, I know what you are thinking, why was this hard, convert to greyscale and then add a transparent overlay and you should be good to go... wrong... it looked awful. Try it, you'll see... awful.

Then I discovered the YIQ color space. This is the color space used by original television broadcasting to get to black and white TV. This was perfect because once it was black and white it was easy to add the color.

So I found this formula (sorry to whomever I originally found it from, but I found it again, it WAS 4 years ago). I copied the code out of the link so you don't have to follow it, but I wanted to give credit to *somebody*

RGB to/from YIQ
The YIQ system is the colour primary system adopted by NTSC for colour television broadcasting. The YIQ color solid is formed by a linear transformation of the RGB cube. Its purpose is to exploit certain characteristics of the human visual system to maximize the use of a fixed bandwidth. The transform maxtrix is as follows


Note: First line Y = (0.299, 0.587, 0.144) (R,G,B) also gives pure B&W translation for RGB. The inverse transformation matrix that converts YIQ to RGB is


So what this looks like in pixel bender is:

kernel colorSepia
<   namespace : "com.squaredi.colorutils";
    vendor : "Drew Shefman";
    version : 2;
    description : "a variable color sepia filter"; >
    parameter float intensity;
    parameter float destColor

    input image4 src;
    output float4 dst;

    // evaluatePixel(): The function of the filter that actually does the 
    //                  processing of the image.  This function is called once 
    //                  for each pixel of the output image.
        // temporary variables to hold the colors.
        float4 rgbaColor;
        float4 yiqaColor;

        // The language implements matrices in column major order.  This means
        // that mathematically, the transform will look like the following:
        // |Y|   |0.299     0.587   0.114   0.0| |R|
        // |I| = |0.596     -0.275  -0.321  0.0| |G|
        // |Q|   |0.212     -0.523  0.311   0.0| |B|
        // |A|   |0.0       0.0     0.0     1.0| |A|
        float4x4 YIQMatrix = float4x4(
            0.299,  0.596,  0.212, 0.000,
            0.587, -0.275, -0.523, 0.000,
            0.114, -0.321,  0.311, 0.000,
            0.000,  0.000,  0.000, 1.000
        // Similar to the above matrix, the matrix is in column order.  Thus, 
        // the transform will look like the following:
        // |R|   |1.0   0.956   0.621   0.0| |Y|
        // |G| = |1.0   -0.272  -0.647  0.0| |I|
        // |B|   |1.0   -1.11   1.70    0.0| |Q|
        // |A|   |0.0   0.0     0.0     1.0| |A|
        float4x4 inverseYIQ = float4x4(
            1.0,    1.0,    1.0,    0.0,
            0.956, -0.272, -1.10,  0.0,
            0.621, -0.647,  1.70,   0.0,
            0.0,    0.0,    0.0,    1.0

        // get the pixel value at our current location
        rgbaColor = sampleNearest(src, outCoord());

        yiqaColor = YIQMatrix * rgbaColor;

        // Here we set the I value of the YIQ color to the intensity
        // specified in the UI.  
        yiqaColor.y = intensity; 
        // zero out the Q to apply the sepia tone
        yiqaColor.z = destColor;

        // convert back to RGBA and set the output value to the modified color.
        dst = inverseYIQ * yiqaColor;

Which in using can give you images like this:

DestColor = -0.16

DestColor = -0.24

DestColor = -0.3

Not bad, eh? (That's me, publicly patting myself on the back :) from 4 years ago )