Modify RGBA Color Channels With The feComponentTransfer Filter Primitive

Often when altering the color of an image you’d like to make one area brighter and another darker or you want to increase the saturation of reds while decreasing the saturation of blues and greens. The feComponentTransfer allows you to work with each channel, including the alpha channel independently of the others.

Last week I started talking about filter primitives to work with color and I walked you through the feColorMatrix primitive. Today I want to continue talking about color filters and walk through the feComponentTransfer filter primitive.

This series builds on an earlier series where I first talked about SVG filters and then some of the details of a few filter primitives. Here’s the earlier series in case you missed it or would like a review

The feComponentTransfer Filter Primitive

When working with feColorMatrix, you might have noticed that the three shortcut types, saturate, hueRotate, and luminanceToAlpha, operate on all three color channels plus the alpha channel at the same time. To work with the channels independently of each other you need to us the matrix type, which can be difficult to work with depending on your comfort with matrices.

The feComponentTransfer primitive provides a way for you to manipulate each of the four color channels separately. It allows for operations to adjust the brightness, contrast, or intensity (color balance) of colors. It also allows for thresholding or image segmentation.

To operate on each of the RGBA channels separately, feComponentTransfer takes a number of subelements, which are the transfer functions, feFuncR, feFuncG, feFuncB, and feFuncA. You only need to include those channels you wish to modify and if you use the same function more than once, whichever comes last will be used.

Each transfer function will have a type attribute and depending on the value of type it will take some additional attributes. The general form when working with feComponentTransfer looks like the following, though again you don’t have to use all four subelements.

1
2
3
4
5
6
<feComponentTransfer in="" result="">
 <feFuncR type="" />
 <feFuncG type="" />
 <feFuncB type="" />
 <feFuncA type="" />
</feComponentTransfer>

There are five possible types for each of the individual functions.

1
type = "identity | table | discrete | linear | gamma"
  • identity sets the resulting value equal to the input value. In other words it doesn’t change it. This is the default for any transfer function that isn’t set.
  • table maps equal segments of a color channel to output ranges which are set using supplied values
  • discrete maps equal segments of a color channel to specific output values set using supplied values
  • linear applies a linear formula to change color values
  • gamma applies a gamma function to change color values

Setting one the transfer functions to identity (or not including it as a subelement) tells the function to not change anything. It’s the easiest type to understand and there are no additional attributes to set. Each of the other types will need a little more explanation.

First here’s what I expect is a familiar image of the Strawberry Fields memorial in Central Park as I’ve used it a few times in the previous series as well as a few times in last week’s post.

I’m including the unfiltered image in case you want to scroll back up for comparison after seeing the filtered image.

type=“table”

Both table and discrete take the same additional attribute, tableValues. Here’s how it might look if you set the red transfer function to type=“table” along with some values for tableValues. Note the values are separated with spaces.

1
2
3
<feComponentTransfer>
 <feFuncR type="table" tableValues="0.0 0.6 0.8 1.0"/>
</feComponentTransfer>

I set four numbers for tableValues, though you can set any amount from 1 to as many as you’d like. The number of values = n. The input color channel is then divided into n–1 (in this case 3) equal ranges.

1
2
3
0.00 - 0.33
0.33 - 0.66
0.66 - 1.00

The ranges are then evenly mapped into the ranges specified by the tableValues. Note that the end points of these ranges are the values I set for tableValues.

1
2
3
0.00 - 0.60
0.60 - 0.80
0.80 - 1.00

Here’s how the remapping works. Say a pixel in the input graphic has a red value of 0.5. The value 0.5 falls at the midpoint of the range 0.33 – 0.66 in the original table. It gets remapped to the midpoint of the same row in the table created by the chosen tableValues, in this case the midpoint of 0.60 – 0.80, or 0.7.

How about an example? I included the image inside an SVG <image> element, set its width and height, and referenced a filter.

The filter includes an feComponentTransfer filter primitive with a single subelement to modify the green change (feFuncG). I set the tables values to 0.0, 0.15, 0.8, and 1.0.

1
2
3
4
5
6
7
8
9
10
11
<svg width="100%" height="495" style="outline: 1px solid red">
 <defs>
   <filter id="table"> 
    <feComponentTransfer>
     <feFuncG type="table" tableValues="0.0 0.15 0.8 1.0" />
    </feComponentTransfer>
   </filter>
 </defs>

 <image xlink:href="http://www.vanseodesign.com/blog/wp-content/uploads/2013/09/strawberry-fields.jpg" width="660" height="495" filter="url(#table)" />
</svg>

Here’s the result. It’s probably not the easiest to see, but the green channel in the image has been remapped to the new values I set on tableValues.

type=“discrete”

The discrete type also remaps pixel channel values based on the tableValues you set, though the remapping is a little different.

1
2
3
<feComponentTransfer>
 <feFuncR type="discrete" tableValues="0.0 0.6 0.8 1.0"/>
</feComponentTransfer>

With discrete the number of ranges = n (instead of n–1).

1
2
3
4
0.00 - 0.25
0.25 - 0.50
0.50 - 0.75
0.75 - 1.00

The ranges are mapped directly to the the values in tableValues

1
2
3
4
0.0
0.6
0.8
1.0

Say a pixel in the input graphic has a red value of 0.60. That value falls within the range 0.50 – 0.75, which is the third range. It gets remapped to the third value, 0.8. Ultimately a range of values all get remapped to a single value, in other words the range is remapped to a discrete value.

One thing to note is the discrete tableValues can be in any order. They don’t have to proceed from 0.0 to 1.0.

1
2
3
<feComponentTransfer>
 <feFuncR type="discrete" tableValues="0.1 0.6 0.2 0.9"/>
</feComponentTransfer>

The code above is valid even though the values increase, then decrease, and then increase again.

Once again, I’ll modify only the green channel in the example and I’ll use the same tableValues as the previous example, 0.0, 0.15, 0.9, and 1.0.

1
2
3
4
5
6
7
8
9
10
11
<svg width="100%" height="495" style="outline: 1px solid red">
 <defs>
   <filter id="discrete"> 
    <feComponentTransfer>
     <feFuncG type="discrete" tableValues="0.0 0.15 0.8 1.0" />
    </feComponentTransfer>
   </filter>
 </defs>

 <image xlink:href="http://www.vanseodesign.com/blog/wp-content/uploads/2013/09/strawberry-fields.jpg" width="660" height="495" filter="url(#discrete)" />
</svg>

Note that the result looks like it uses less colors with discrete as the type. That’s because it does. The discrete type turns ranges of values into a single value. Think of it like reducing the number of colors in a .png file when saving for the web in your image editor of choice.

type=“linear”

When type=“linear” the function uses the following linear equation to modify the color of each pixel in the specific color channel.

1
C' = slope * C + intercept

C’ is the final pixel value, C is the input pixel value and slope (default = 1) and intercept (default = 0) are values you set using the slope and intercept attributes. The value of intercept provides a base value for the result and the slope changes the scale of the function. Increasing either, increases how much of that particular channel is in the resulting pixel.

Here’s an example where I use all four color channels with the linear type and different values for the slope and intercept.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<svg width="100%" height="495" style="outline: 1px solid red">
 <defs>
   <filter id="linear"> 
    <feComponentTransfer>
     <feFuncR type="linear" slope="0.5" intercept="0.0" />
     <feFuncG type="linear" slope="1.0" intercept="0.1" />
     <feFuncB type="linear" slope="1.5" intercept="0.2" />
     <feFuncA type="linear" slope="2.0" intercept="0.3" />
    </feComponentTransfer>
   </filter>
 </defs>

 <image xlink:href="http://www.vanseodesign.com/blog/wp-content/uploads/2013/09/strawberry-fields.jpg" width="660" height="495" filter="url(#linear)" />
</svg>

I won’t pretend there was any specific reasons for the values I chose in this example, however, here’s the result. Note that while I’ve used all four channel functions in this example, you would more likely use feComponentTransfer to tweak specific channels as opposed to all of them.

Here’s another example where I used the default values for slope and intercept to make the image brighter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<svg width="100%" height="495" style="outline: 1px solid red">
 <defs>
   <filter id="linear"> 
     <feComponentTransfer>
       <feFuncR type="linear" slope="1.0" intercept="0.0" />
       <feFuncG type="linear" slope="1.0" intercept="0.0" />
       <feFuncB type="linear" slope="1.0" intercept="0.0" />
       <feFuncA type="linear" slope="1.0" intercept="0.0" />
     </feComponentTransfer>
   </filter>
 </defs>

 <image xlink:href="http://www.vanseodesign.com/blog/wp-content/uploads/2013/09/strawberry-fields.jpg" width="660" height="495" filter="url(#linear)" />
</svg>

And here’s the result.

type=“gamma”

When type=“gamma”, the function uses an exponential function to modify the pixel values in the specific color channel.

1
C' = amplitude * pow(C, exponent) + offset

C’ is again the result and C is the original input with amplitude (default = 1), exponent (default = 1), and offset (default = 0) all being additional attributes with values you set.

As in the linear example, here’s an example with type set to gamma and various values for amplitude and exponent on all four channels.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<svg width="100%" height="495" style="outline: 1px solid red">
 <defs>
   <filter id="gamma"> 
    <feComponentTransfer>
     <feFuncR type="gamma" amplitude="0.5" exponent="0.3" offset="0.0" />
     <feFuncG type="gamma" amplitude="0.75" exponent="0.5" offset="0.0" />
     <feFuncB type="gamma" amplitude="1.5" exponent="0.7" offset="0.0" />
     <feFuncA type="gamma" amplitude="1" exponent="1" offset="0.0" />
    </feComponentTransfer>
   </filter>
 </defs>

 <image xlink:href="http://www.vanseodesign.com/blog/wp-content/uploads/2013/09/strawberry-fields.jpg" width="660" height="495" filter="url(#gamma)" />
</svg>

Again I won’t pretend there were specific reasons for my chosen values. Just arbitrary numbers to see what happened. Here’s the result.

As with type=“linear” you would likely modify only those color channels you wish to change as opposed to modifying all of them as I have in the example.

Here’s another example using default values for amplitude, exponent, and offset to again make the image brighter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<svg width="100%" height="495" style="outline: 1px solid red">
 <defs>
   <filter id="gamma"> 
     <feComponentTransfer>
       <feFuncR type="gamma" amplitude="1.0" exponent="1.0" offset="0.0" />
       <feFuncG type="gamma" amplitude="1.0" exponent="1.0" offset="0.0" />
       <feFuncB type="gamma" amplitude="1.0" exponent="1.0" offset="0.0" />
       <feFuncA type="gamma" amplitude="1.0" exponent="1.0" offset="0.0" />
     </feComponentTransfer>
   </filter>
 </defs>

 <image xlink:href="http://www.vanseodesign.com/blog/wp-content/uploads/2013/09/strawberry-fields.jpg" width="660" height="495" filter="url(#gamma)" />
</svg>

And the result.

Closing Thoughts

Like feColorMatrix, feComponentTransfer offers a lot of flexibility when adjusting color values. Unlike feColorMatrix feComponentTransfer makes it easier to modify the different channels independently of each other.

It offers several different ways to adjust the color values and I admit it’s not immediately intuitive what will happen based on the values you set. Trial and error definitely help. Apply a filter to an image using the feComponentTransfer primitive. Use one of the different types and modify a single attribute value holding the rest constant. It’ll be easier to predict what will happen once you have a feel for what the values do. The need for trial and error shouldn’t be surprising given it’s the best way to learn how color works.

Next week I want to change focus and start talking about different lighting effects you can create with SVG filters using a couple of different primitives, feDiffuseLighting and feSpecularLighting. First, though, we need to talk about different types of light sources.

Download a free sample from my book, Design Fundamentals.

Leave a Reply

Your email address will not be published. Required fields are marked *