[ Beneath the Waves ]

Secrets of Photoshop's Colour Blend Mode Revealed (Sort Of)

article by Ben Lincoln

 

Back when I originally started writing The Mirror's Surface Breaks, I knew that I had to come up with a way to emulate the "Colour" blend mode from Adobe Photoshop®. I'd been extremely happy with some of the false colour images I'd generated by applying e.g. the colour of the RGB variation to the luminance of the near infrared version of the same shot, and if TMSB couldn't handle that for me, I'd be stuck doing a bunch of repetitive, manual work for hours. If I'd known how much trouble it was going to be to implement, I might not have even started, but since I did, hopefully others will benefit from the knowledge I obtained.

First, let's catalogue what the "Colour" blend mode does not do, in the order that my theories were formed and then disproven through testing:

If you are very well-read in this area, you may suspect (or even know) that the HSY (hue/saturation/luminance) colourspace is where the work takes place. However, what you probably don't know is that it's still not just a combination of the H and S channels of one image with the Y channel of a second. No, that would be too easy. That method also doesn't bring quite as much of the contrast of the luminance image through to the final result.

After some extensive fiddling around, reading Adobe's PDF specification, and discussing this topic in email (thanks, Andrew!) I can finally describe how to accurately mimic what Photoshop® does when this blend mode is used. You can read about the original method used by TMSB in the Appendix A section near the end of this article.

Adobe's PDF specification (specifically, PDF Blend Modes: Addendum) describes the colour blend mode as a set of three functions, which I've paraphrased here as pseudocode (but not the same pseudocode as in the original document), with pixel being a struct of three floating point values (Red, Green, and Blue).

float Lum(pixel colour)

{

return (colour.Red * 0.3) + (colour.Green * 0.59) + (colour.Blue * 0.11);

}

 

pixel ClipColour(pixel colour)

{

pixel result;

float luminance = Lum(colour);

float cMin = min(colour.Red, colour.Green, colour.Blue);

float cMax = max(colour.Red, colour.Green, colour.Blue);

if (cMin < 0.0)

{

result.Red = luminance + (((colour.Red - luminance) * luminance) / (luminance - cMin));

result.Green = luminance + (((colour.Green - luminance) * luminance) / (luminance - cMin));

result.Blue = luminance + (((colour.Blue - luminance) * luminance) / (luminance - cMin));

}

if (cMax > 1.0)

{

result.Red = luminance + (((colour.Red - luminance) * (1.0 - luminance)) / (cMax - luminance));

result.Green = luminance + (((colour.Green - luminance) * (1.0 - luminance)) / (cMax - luminance));

result.Blue = luminance + (((colour.Blue - luminance) * (1.0 - luminance)) / (cMax - luminance));

}

return result;

}

 

pixel SetLum(pixel colour, float luminance)

{

pixel result;

float diff = luminance - Lum(colour);

result.Red = colour.Red + diff;

result.Green = colour.Green + diff;

result.Blue = colour.Blue + diff;

return ClipColour(colour);

}

These functions together define the colour blend mode (in which the upper layer is passed to SetLum as the first variable, and the lower layer is passed as the second), and the luminance blend mode (with the upper layer passed as the second variable, and the lower layer the first).

It took me awhile to get comfortable enough with the DaVinci script language to write this function in a way that didn't use for loops to iterate through each pixel, which is extremely slow in DaVinci. Once I finally wrapped my head around how to handle this using sets instead of individual values, this was the result, which I've simplified somewhat for presentation here. base represents the image to be used for the luminance, and colour the image to be used for the colour.

For those who are unfamiliar with the DaVinci script language, quirks like defining a variable named "one" are due to the way DaVinci handles mathematical operations involving sets of data[4].

basey = rgb2hsy(tofloat(base))[0,0,3]

coloury = rgb2hsy(tofloat(colour))[0,0,3]

 

result = colour + (basey - coloury)

 

colourmin = min(result, axis = z)

colourmax = max(result, axis = z)

 

resultmin = threeclone(basey)

resultmax = resultmin

 

resultmin = resultmin + (((result - basey) * basey) / (basey - colourmin))

 

one = basey

one[0,0,0] = 1.0

 

resultmax = resultmax + (((result - basey) * (one - basey)) / (colourmax - basey))

 

minmask = ceil(abs(clipdata(colourmin, min=-1.0, max=0.0, quiet=1)))

maxmask = ceil(clipdata((colourmax - 1.0), min=0.0, max=1.0, quiet=1))

one = maxmask

one[0,0,0] = 1.0

noclipmask = (one - maxmask) - minmask

# in the PDF specification, the max case takes precedence over the min case

minmask = clipdata(minmask - maxmask, min=0.0, max=1.0, quiet=1)

 

result = (result * noclipmask) + (resultmin * minmask) + (resultmax * maxmask)

 

return(result)

For purposes of illustration, here are a series of example images, with the results of blending the colours using the following methods:

Note: if you wish to do your own comparisons, you must change Photoshop®'s colour settings so that the grey working space is "Gray Gamma 2.2". The default value is "Dot Gain 20%", which boosts the levels of greyscale images, and will throw off the results.

Luminance/Colour Example Set 1
[ Kurtosis [Grey] ]
Kurtosis [Grey]
[ R-G-B ]
R-G-B
[ HSL Blend ]
HSL Blend
[ HSV Blend ]
HSV Blend
[ HSY Blend ]
HSY Blend
[ IHSL Blend ]
IHSL Blend
[ YUV Blend ]
YUV Blend
[ Photoshop Blend ]
Photoshop Blend
[ Original TMSB Blend ]
Original TMSB Blend
[ The saturation channel as blended by Photoshop ]
The saturation channel as blended by Photoshop
[ The saturation channel as blended by TMSB ]
The saturation channel as blended by TMSB
[ The saturation channel from a simple HSY blend ]
The saturation channel from a simple HSY blend
[ Updated TMSB Blend ]
Updated TMSB Blend
[ Mask for the updated TMSB blend ]
Mask for the updated TMSB blend
 

This set shows the cube kurtosis used as the luminance and the red/green/blue version used as the colour. The blend mask shown in the final version of the image represents the three masks used in the updated TMSB blend method. Areas that are green use a straight luminance channel swap in HSY colourspace. Areas that are red had a minimum value less than 0 in the intermediate stage. Areas that are blue had a maximum value greater than 1 in the intermediate stage.

Date Shot: 2010-10-16
Camera Body: Nikon D70 (Modified)
Lens: Nikon Micro-Nikkor 105mm f/4
Filters: Standard Set
Date Processed: 2011-01-16
Version: 1.0

 

In this first set of example images, you can see the sort of difference in red saturation that I described earlier.

Luminance/Colour Example Set 2
[ Average Deviation [Grey] ]
Average Deviation [Grey]
[ G-B-UVA [3C] ]
G-B-UVA [3C]
[ HSL Blend ]
HSL Blend
[ HSV Blend ]
HSV Blend
[ HSY Blend ]
HSY Blend
[ IHSL Blend ]
IHSL Blend
[ YUV Blend ]
YUV Blend
[ Photoshop Blend ]
Photoshop Blend
[ Original TMSB Blend ]
Original TMSB Blend
[ Updated TMSB Blend ]
Updated TMSB Blend
[ Mask for the updated TMSB blend ]
Mask for the updated TMSB blend
       

This set shows the cube average deviation used as the luminance and the Green/Blue/Ultraviolet-A false colour image used as the colour. The blend mask shown in the final version of the image represents the three masks used in the updated TMSB blend method. Areas that are green use a straight luminance channel swap in HSY colourspace. Areas that are red had a minimum value less than 0 in the intermediate stage. Areas that are blue had a maximum value greater than 1 in the intermediate stage.

Date Shot: 2010-10-16
Camera Body: Nikon D70 (Modified)
Lens: Nikon Micro-Nikkor 105mm f/4
Filters: Standard Set
Date Processed: 2011-01-16
Version: 1.0

 

No big surprises in this second set of images, except that the YUV method is beginning to show signs of weakness which will become glaringly obvious shortly.

These next two combinations were the original test cases I used when writing my own colour-blending functionality for TMSB.

Luminance/Colour Example Set 3
[ NIR [Grey] ]
NIR [Grey]
[ R-G-B ]
R-G-B
[ HSL Blend ]
HSL Blend
[ HSV Blend ]
HSV Blend
[ HSY Blend ]
HSY Blend
[ IHSL Blend ]
IHSL Blend
[ YUV Blend ]
YUV Blend
[ Photoshop Blend ]
Photoshop Blend
[ Original TMSB Blend ]
Original TMSB Blend
[ Updated TMSB Blend ]
Updated TMSB Blend
[ Mask for the updated TMSB blend ]
Mask for the updated TMSB blend
       

This set shows the near infrared channel used as luminance, and the red/green/blue image used for colour. The blend mask shown in the final version of the image represents the three masks used in the updated TMSB blend method. Areas that are green use a straight luminance channel swap in HSY colourspace. Areas that are red had a minimum value less than 0 in the intermediate stage. Areas that are blue had a maximum value greater than 1 in the intermediate stage.

Date Shot: 2009-08-31
Camera Body: Nikon D70 (Modified)
Lens: Nikon Series E (unknown focal length)
Filters: Standard Set
Date Processed: 2011-01-16
Version: 1.1

 

YUV provides very little "colour" here. In addition, notice how the HSL version has introduced a couple of artifacts into the image.

Luminance/Colour Example Set 4
[ R-G-B (Luma) [Grey] ]
R-G-B (Luma) [Grey]
[ Green ]
Green
[ HSL Blend ]
HSL Blend
[ HSV Blend ]
HSV Blend
[ HSY Blend ]
HSY Blend
[ IHSL Blend ]
IHSL Blend
[ YUV Blend ]
YUV Blend
[ Photoshop Blend ]
Photoshop Blend
[ Original TMSB Blend ]
Original TMSB Blend
[ Updated TMSB Blend ]
Updated TMSB Blend
[ Mask for the updated TMSB blend ]
Mask for the updated TMSB blend
       

This set shows the red/green/blue image used for luminance, and a simple green tint used for colour. The blend mask shown in the final version of the image represents the three masks used in the updated TMSB blend method. Areas that are red had a minimum value less than 0 in the intermediate stage. Areas that are blue had a maximum value greater than 1 in the intermediate stage. There are no areas of this image that were not clipped, so not part of the mask image is green.

Date Shot: 2009-08-31
Camera Body: Nikon D70 (Modified)
Lens: Nikon Series E (unknown focal length)
Filters: Standard Set
Date Processed: 2011-01-16
Version: 1.1

 

Finally, YUV cracks completely under the strain of trying to apply the colour of a green image to a greyscale luminance. Green seems to be one of the more challenging colours for this type of algorithm - possibly because our eyes are so much more sensitive to it than red or blue. You can see that except for the Photoshop® and TMSB methods, every other version has a noticeable problem - a green tint that makes the highly-reflective water appear darker, worse contrast, etc.

Note

There are a few different options available to TMSB users who would prefer to use either a straight luminance channel swap in HSY colourspace or the "classic" TMSB method. Both involve editing dshadow.dvrc (located in the library subdirectory) with a text editor. Look in the blendcolourrgb(), blendcolourhsy(), and blendcolourpdfspec() functions for the lines which are marked "# uncomment the next line", "# uncomment the next two lines", and so forth. Remove the pound signs (#) from the beginning of the lines after that as indicated to switch to the other mechanism.

Appendix A: The Original TMSB Method

Prior to June of 2011, the method used by TMSB was this, which is preserved for historical reasons:

Given two images (A and B), if the "colour" of A is to be applied to the "luminance" of B, the resulting image has:

This is the script code that is used in DaVinci's Shadow for the original method. DaVinci is heavily optimized for use with set-based operations, so this code does not use for loops.[3]

y = base[0,0,3]

h = colour[0,0,1]

s = base[0,0,3]

cy = colour[0,0,3]

 

# delta from colour luminance

s = s - cy

spos = s

spos[0,0,1] = 0.0

spos = cat(spos, s, axis=z)

spos = max(spos, axis=z)

 

sneg = s

sneg[0,0,1] = 0.0

sneg = cat(sneg, s, axis=z)

sneg = abs(min(sneg, axis=z))

 

# rescale the delta values

sposceil = cy

sposceil[0,0,1] = 1.0

sposceil = sposceil - cy

spos = spos / sposceil

 

sneg = sneg / cy

 

cy = 0

 

s = cat(spos, sneg, axis=z)

 

spos = 0

sneg = 0

 

s = max(s, axis=z)

 

# clip saturation to a range of 0 - 1

s2 = s

s2[0,0,1] = 1.0

s = cat(s, s2, axis=z)

s = min(s, axis=z)

s2[0,0,1] = 0.0

s = cat(s, s2, axis=z)

s = max(s, axis=z)

# end clip

 

# invert the calculated saturation value

s2 = s

s2[0,0,1] = 1.0

s = s2 - s

s2 = 0

# end inversion

 

# cap the saturation at the value from the colour image

s = cat(s, colour[0,0,2], axis=z)

s = min(s, axis=z)

# end cap

This does not result in an exact match, but it is virtually identical in most cases. The main difference tends to be that this algorithm will sometimes produce slightly higher red saturation than the one used in Photoshop®. However, it preserves the main advantage of the Photoshop® function, which is that both of them provide more constrast in the resulting image than a straight luminance-replacement.

 
Footnotes
1. The GIMP does use the HSL method, but that's a different piece of software, and it gives different results.
2. HSY is also known as IHSL ("improved hue/saturation/lightness"), but (to my knowledge) IHSL uses the luminance coefficients from the YCrCb standard, whereas HSY uses the coeffecients from YUV.
3. The RGB <-> HSY code was based on the equations published in "A 3D-polar Coordinate Colour Representation Suitable for Image Analysis" (Allan Hanbury and Jean Serra, 2002).
4. The need for this quirkiness seems to be reduced in newer versions of DaVinci, so I may remove it in the future.
 
[ Page Icon ]