ImageMagick v6 Examples --
Color Modifications

Index
ImageMagick Examples Preface and Index
Converting Color to Gray-Scale (Making grayscale images)
Image Level Adjustments (adjusting the colors in images)
Adjustments Using Histogram Modification (changing the histogram an image)
DIY Level Adjustments (general tinting operators)
Tinting Midtones of Images (general tinting operators)
Global Color Modifiers
Recoloring Images with Lookup Tables
Replacing Colors in an Image (replacing individual colors)
Color modification of images without changing the overall image itself is a very common requirement of ImageMagick. Whether it is to lighten or darken the image, or more drastic color modifications.

We will need a test image... Don't worry above how I actually generated this image, it is not important for the exercise. I did design it to contain a range of colors, transparencies and other features, specifically to give IM a good workout when used. [IM Output]

If you are really interested in the commands used to generate this image you can look at the special script, "generate_test", I use to create it.


Converting Color to Gray-Scale

Gray scale images can be very useful for many uses, such as, furthering the processing of the original image or for use in background compositions. The best method of converting an image to gray-scale is to just ask IM to convert the image into a gray-scale Color Space representation for the image.

  convert  test.png  -colorspace Gray   gray_colorspace.png
[IM Output] ==> [IM Output]

Note how the blue is much darker than the red, due the weighting to match the intensity as they seem to appear to the human eye. That is, 'red' is quite a bright color compared to 'blue' which looks darker.


However there a many other methods, and meanings of 'gray-scale'...

For example, you can drain all the color out of the image by using "-modulate", to set all color saturation levels to zero.

  convert test.png  -modulate 100,0  gray_modulate.png
[IM Output]

Note how the IM 'green' color I used for the center colored disk in my test image is not actually a pure green, such as used in the colored rainbow, but the half-bright green defined by the new SVG -- Scalable Vector Graphics standard. If you need a pure RGB green you can use the color 'lime' instead.

Another way is to use the FX DIY operator to average the three channels together to get a pure mathematical meaning of gray-scale.

  convert test.png -fx '(r+g+b)/3' gray_fx_average.png
[IM Output]

You can use the same technique to control the weighting of the individual color channels. For example this is the formula used by IM to convert RGB colors to gray-scale...

  convert test.png -fx '0.3*r+0.6*g+0.1*b' gray_diy.png
[IM Output]

You can also use 'intensity' if you want the same meaning within the "-fx" operator.

  convert  test.png  -fx intensity  gray_intensity.png
[IM Output]


Another technique is to simply add all three channels together (a color measure known as manhatten distance) and while the resulting image will not loose information due to 'quantum rounding' effects, you may loose information about the brightest colors. Unfortunately, you also loose the transparency channel, too.

  convert test.png -separate \
          -background black -compose plus -flatten   gray_added.png
[IM Output]

This grayscale image is particularly well suited for generating masks from Difference Images.

However as the FX DIY operator is interpreted, it can run very very slowly. For more complex operations you can use the simpler Evaluate Operator, "-evaluate".

For example here is a 2/5/3 ratio gray-scaled image, though again I make no attempt to preserve the transparency channel of the original image.

  convert test.png -channel R -evaluate multiply .2 \
                   -channel G -evaluate multiply .5 \
                   -channel B -evaluate multiply .3 \
                   +channel -separate -compose add -flatten gray_253.png
[IM Output]

The above would suffer from 'quantization' effects for a ImageMagick compiled at a 'Q8' Quality Level. That is because the results of the "-evaluate" will be saved into a small 8 bit integer, used for image values. Only later are those values added together with the resulting loss of accuracy.

An ImageMagick compiled with 'Q16', or better still the HDRI, quality compile options will produce a much more exact result.

A similar technique can be used to generate a pure mathematical gray-scale, by directly averaging the three RGB channels equally.

  convert test.png -separate -average  gray_average.png
[IM Output]

However as you can see, I did not attempt to preserve the alpha channel of the resulting image.

Another fast alternative is to use the "-recolor" color matrix operator, which will let you specify the weighting of the three color channels.

  convert test.png -recolor '.2 .5 .3
                             .2 .5 .3
                             .2 .5 .3'   gray_recolor.png
[IM Output]

This doesn't affect transparency, but makes it a much better way of converting colors using a specific weighting.

Basically the first tree numbers is the channel weighting for the resulting images red channel, next 3 for green, and the final three numbers for blue.


A much more interesting technique is to extract a variety of different meanings of brightness by extracting the appropriate Color Channel from various Color Space representations of the image. The first image is the normal recommended method.

  convert rose: -colorspace Gray                      channel_gray.gif
  convert rose: -colorspace CMYK -channel K -negate -separate channel_black.gif
  convert rose: -colorspace HSB  -channel B -separate channel_brilliance.gif
  convert rose: -colorspace HSL  -channel B -separate channel_lightness.gif
  convert rose: -colorspace YUV  -channel R -separate channel_luma.gif
[IM Output] ==> [IM Output]
Gray
RGB
[IM Output]
Neg Black
CMYK
[IM Output]
Brilliance
HSB
[IM Output]
Lightness
HSL
[IM Output]
Luma (Y)
YUV

Note that none of the gray-scale results are quite the same due to the different meanings of 'brightness' in the various colorspaces.

Alternately you can use "-type" to tell IM to treat the image as gray-scale, when either reading or writing the image.

  convert  test.png  -type GrayScaleMatte  gray_type.png
[IM Output]

The "-type" setting is generally only used when an image is being read or written to a file. As such its action is delayed to the final write of the image. Its effect is also highly dependant on the capabilities of the image file format involved and is used to override ImageMagick's normal determination during that process. See the Type examples for more information.

Before IM v6.3.5-9 the above will have removed any transparency in the written image (equivalent of a "-type Grayscale") due to a bug. This was fixed as soon as I noted the problem and reported it. (There is a lesson here :-)


Image Level Adjustments

The most basic form of adjustment you can make to images are known as 'level' adjustments. This basically means taking the individual RGB color values (or even the matte/alpha channel values) and adjusting them so as to either stretch or compress those values.

As only channel values are being adjusted, they are best demonstrated on a gray-scale image, rather than a color image. However if you adjust all the color channels of an image by the same amount you can use them with color images, for the purposes of either enhancing, or adjusting the image. Do not confuse this with the more automatic form of level adjustments, which we will look at in the next major section of examples below, Auto-Level Adjustments. This function will do exactly the same operation regardless of the actual content of the image. It does not matter if the image is bright, or dark, or has a blue, or yellow tint. The operations are blind to the actual image content.

[IM Graph] In demonstrating these operations I will be using a modified "gnuplot" graph such as shown to the right, which I generate using a special script "im_graph". The graph has a red line which maps the given original 'x' value (representing the gray-scale value of the top most gradient) to the 'y' value shown. The resulting color gradient is also shown underneath the input linear gradient.

The graph shown to right is of the IM "-noop" operator which actually does nothing to an image. As such each of the image's color values are just mapped to exactly the same value without change. The lower gradient is thus the same as the upper gradient.

Image Negation

The simplest and most basic global level adjustment you can make is to negate the image, using the "-negate" image operator.

Essentially this makes   white, black,   and   black, white,  , adjusting all the colors to match. That is, it will make the color red, its complementary color of cyan,   and blue, yellow, etc.

You can see this with the mapping graph shown below, as I use the "-negate" operator on both the 'test' image and the standard IM 'rose' built-in image. Note how the lower gradient in the mapping graph image is now reversed, so that black and white are swapped, and the same reversal appearing in the negated 'test' image.

  convert  test.png  -negate  test_negate.png
  convert  rose:     -negate  rose_negate.gif
[IM Output]
[IM Output]
==> [IM Graph] ==> [IM Output]
[IM Output]

Internally negate is actually rather stupid. It handles the three color channels independently, and by default ignores the alpha or matte channel. If this was not the case, you would get a very silly result like this...

  convert  test.png -channel RGBA  -negate  negate_rgba.png
[IM Output] ==> [IM Output]

The image is negated, as you can see by the semi-transparent color gradient. But as the transparency channel has also been negated you loose all the opaque colors in the image. This is why the default setting for "-channel" is 'RGB'. See Color Channels for more information.

You can limit the negation to just one channel, say the green color channel. This may not seem very useful, but at times it is vitality important.

  convert  test.png -channel green  -negate  negate_green.png
[IM Output] ==> [IM Output]

The "-negate" operator is actually its own inverse. Doing two negations with the same "-channel" setting cancels each other out.

  convert  negate_green.png  -channel green  -negate  negate_restore.png
[IM Output] ==> [IM Output]

Negation is extremely common in image processing, particularly when dealing with gray-scale images as a step before or after other processing options. As such I recommend you play with it and keep it in mind whenever you are doing anything, as working with negated images can solve some otherwise difficult problems.

Direct Level Adjustments

The "-level" operator is the more general level adjustment operator. You basically give it two values a 'black_point' and a 'white_point', as well as an optional third value (gamma adjustment), which I will look at later.

What it does is map any color values in the image that is equal to or less than the 'black_point', and make them black (or a 0 value). Similarly, any color values that are equal to or brighter that the 'white_point' will make them white (or a Maximum value). The colors in between these two points are then 'stretched' linearly to fill the complete range of values.

The effect of this is to improve the contrast, enhancing the colors within an image. For example here is a 25% contrast enhancement of our test image, using the same values as shown by the graph.

As you commonly adjust both the black and white points by the same amount from the 0% and 100% amounts, you can just specify the 'black_point' only. The white point will be adjusted by the same amount inward.

  convert  test.png  -level 25%,75%  test_level.png
  convert  rose:     -level 25%      rose_level.gif
[IM Output]
[IM Output]
==> [IM Graph] ==> [IM Output]
[IM Output]

Note that 25% is a huge contrast enhancement for any image, but it clearly shows what it does.

You don't have to change both the 'black' and 'white' points. Instead it is quite permissible to just adjust only one end of the color range. For example we can make a very light, or a very dark rose image.

  convert  rose:  -level 0,75%     rose_level_light.gif
  convert  rose:  -level 25%,100%  rose_level_dark.gif
[IM Graph] ==> [IM Output]    [IM Graph] ==> [IM Output]

However I again warn you that the colors outside the given range are 'clipped' or 'burned', and as such will no longer be available for later image processing. This is the biggest problem with using a "-level" operator.

[IM Graph] By using a negative value you can do some rough de-contrasting of an image.

What this means is that rather than providing a color value for the values to be mapped to 'black' and 'white' and thus stretching out the colors in between, you instead compress the color values so as to map the imaginary negative color to black or white. The result is a general graying of the image.

  convert  rose:  -level -25%  rose_decontrast.gif
[IM Output]

This method of de-contrasting an image however is very inaccurate and not recommended, unless you have a IM older than version 6.4.2 where you don't have access to the new Reversed Level Operator.

[IM Graph] You can use the "-level" operator to negate an image, just by swapping the 'black' and 'white' point values given, using "-level 100%,0".
[IM Graph] Or by setting them to the same value, you can effectively call all the color values in the image to be thresholded. Using "-level" to threshold an image is exactly the same as if you used a Threshold Operator with that value. The mapping graph shown right, shows the results of a "-level 50%,50%" operation, and its effect on a grayscale gradient.


And here is the result of applying this thresholding "-level" operation on the built-in rose image. Note that unlike "-threshold" the image is not automatically converted to a grayscale image when used with the default "-channel" setting.

  convert  rose:  -level 50%,50%  rose_level_thres.gif
[IM Output]

The general nature of using level to linearly modify an image, makes the "-level" operator good for general gray-scale image modifications, and mask adjustments. Add the fact that you can modify individual channels (using the "-channel" setting) as opposed to the whole image, makes it one of the best color modification operators available to IM users.

Note you can also use the Evaluate and Function Operators for a more direct mathematical modification of the color values, to achieve the same results for -level both + and - forms).

Be warned that the "-level" operator treats the transparency channel as 'matte' values. As such 100% is fully transparent and 0% is opaque. Please take this into account when using "-level" with a blurred shape image. This is most typically done after blurring an 'shape' image, to expand and stretch the results. For examples of this see Soft Edges, and Shadow Outlines.

Reversed Level Adjustments

As of IM version 6.4.2 the Level Operator was expanded to provide a 'reversed' form "+level" (note the 'plus'). Alternatively you can use the original "-level" form of the operator but add a '!' to the level argument given (for older API interfaces).

The arguments for this variant is exactly the same, but instead of stretching the values so as to map the 'black_point' and 'white_point' to 'black' and 'white', it maps 'black' and 'white' to the given points. In other words "+level" is the exact reverse of "-level".

For example here we map 'black' to a 25% gray, and white to 75% gray, effectively de-contrasting the image in a very exact way, using the two methods of specifying the 'reversed' form.

  convert  test.png   +level 25%    test_level_plus.png
  convert  rose:      -level 25%\!  rose_level_plus.gif
[IM Output]
[IM Output]
==> [IM Graph] ==> [IM Output]
[IM Output]

If you compare the above "+level 25%" operation with the use of a a negative de-contrasting, "-level -25%" operator we showed previously, you will see that are not the same. The 'plus' version produces a much stronger de-contrasted image (it is greyer), but does so by mapping to the exact values you give the operator, and not the 'imaginary' values the 'minus' form used. This exact value usage is important, and one of the reasons why the 'plus' form of the operator was added.

Of course a '25%' is again a very large value, and it is not recommended for use with typical image work.

Note that the "-level" and "+level", are in actual fact the exact reverse of each other when given the same argument. That is, one maps values to the range extremes, while the other maps from the range extremes.

However while you can use one to 'undo' the other, the result may not be exactly the same, due to 'clipping' and 'quantum rounding' effects on the image values.

For example here we compress the colors of the test image using "+level", then decompress them again using "-level", so as to restore the image close to its original appearance.

  convert  test.png  +level 20%  -level 20%  test_level_undo.png
[IM Output] ==> [IM Graph] ==> [IM Output]

The two images appear to be very very similar, and as I am using a high quality 'Q16' version of IM, you will be hard pressed to notice any difference at all.

However the values may not be exactly the same (especially with a Q8 version of IM), as you have effectively compressed the color values of the image to a smaller range of integers, and then restored them again. This can result some 'rounding' effects.

For example here I used "+level" to compress the color values to a range of only 16 values!

  convert  test.png  +level 0,15  -level 0,15  test_level_quantum.png
[IM Output] ==> [IM Graph] ==> [IM Output]

Notice the sever rounding (quantum effects) that is now visible, forming steps in the operator 'graph'. As only 16 gray-level values were used, you effectively converted the image to a color depth of only 4 bits!

It is unlikely you will come across such sever quantum effects, but they can happen in special situations.


Doing these two operations in the opposite order (stretch, then compress the color values) produces a very different result...

  convert  test.png  -level 20%  +level 20%  test_level_undo2.png
[IM Output] ==> [IM Graph] ==> [IM Output]

Notice how the center values of the color range are restored correctly, but the bright and dark ends have been 'clipped' during the stretching by the "-level" operator. That is the color values either became negative, or went beyond the maximum range (known as 'QuantumRange') of the integers used to store them.

As a result when the "+level" operator was applied the 'clipped' or 'burned' values were moved to the given grey levels, effectively removing the brightest and darkest pixels in the image. This can be either good, or bad depending on what you are trying to do.


The HDRI version of ImageMagick is however designed to combat both of 'rounding' and 'clipping' effects caused by storing values in integers. By using floating point values to store the color values of the image, neither 'rounding', nor 'clipping' effects will cause problems. Floating point numbers can hold fractions of a value, as well as negative or very large positive values.

However in very special extreme cases, even HDRI can fail, typically when a very large 'bias' value is used when dealing with very small values. That is nothing is perfect, but HDRI does bring near-perfection to image color quality.


One other useful aspect of the "+level" operator is that you can completely compress all the color values in an image to the same gray-scale level.

  convert  test.png  +level 30%,30%  test_level_const.png
[IM Output] ==> [IM Graph] ==> [IM Output]

By specifying levels according to the values of specific colors for each individual channel, you can effectively convert a greyscale gradient into a specific color gradient. However this is rather difficult to calculate and do. As such a "-level-colors" operator has also been provided that will let you specify the black and white points in terms of specific colors rather than 'level' values. See Level by Color below.

Level Gamma Adjustments

Both the above "-level" variants also allow you to use a third setting. The 'gamma' adjustment value. By default this is set to a value of 1.0', which does not do any sort of mid-tone adjustment of the resulting image, producing a pure linear mapping of the values from the old image to the new image.

However by making this value larger, you will curve the resulting line so as to brighten the image, while shrinking that value will darken the image.

For example here I use just the 'gamma' setting to brighten and darken just the mid-tones of the image.

  convert  rose:  -level 0%,100%,2.0   rose_level_gamma_light.gif
  convert  rose:  -level 0%,100%,0.5   rose_level_gamma_dark.gif
[IM Graph] ==> [IM Output]    [IM Graph] ==> [IM Output]

Values generally range from 10 for a blinding bright image, to .2 for very dark image. As mentioned a value of 1.0 will make no 'gamma' changes to the image. However the special value of '2.0' (see above) can be used to get the square root of the images normalized color.

Both versions of the "-level" operate handles 'gamma' in the same way. This means you can combine the level adjustment of the 'black' and 'white' ends with a non-linear 'gamma' adjustment. You can also only adjust a single channel of an image. For example, here we give an image a subtle tint at the black end of just the blue channel, while using gamma to preserve the mid-tone color levels of the image.

  convert  test.png  -channel B +level 25%,100%,.6 test_blue_tint.png
[IM Output] ==> [IM Graph] ==> [IM Output]

This specific example could be used to tint a weather satellite photo, where only the sea is pure black, while land is more grey. Other alternatives to this blue channel adjustment are given below in DIY Mathematical Non-linear Adjustments.

Gamma Operation Adjustments

The "-gamma" operator is also provided, and has exactly the same effect as the 'gamma' setting in the "-level" operator. However it will let you adjust the 'gamma' adjustment level for each individual channel as well.

For example here we brighten the image differently for each individual RGB channel.

  convert  rose:  -gamma 0.8,1.3,1.0  gamma_channel.gif
[IM Output]

As you can see this can be used to do some subtle tinting and color adjustments to an image, or correct images with contain too much of a specific color.

One of the most important things when resizing, filtering or modifying images (even more important anything else) is to do it in linear space, so if your image is gamma corrected, you should transform it to linear space, scale and then transform back to gamma space.

One less obvious use of "-gamma" is to zero out specific image channels (see Zeroing Color Channels). Or color an image completely 'black', 'white' or some other primary color (see Primary Colored Canvases).

Level Adjustment by Color

The "-level-colors" operator was added to IM v6.2.4-1. Essentially, it is exactly the same as the Level Operator we discussed above, but with the value for each channel specified as a color value.

That is, the "-level-colors" option will map the given colors to 'black' and 'white' and stretching all the other colors between them linearly. This effectively removes the range of colors given from the image.

And while this works, it is not particularly useful, as it is prone to fail for colors that have common values in some channel. For example, the colors 'DodgerBlue' and 'White' have the same color values in the blue channel. As such, "-level-colors DodgerBlue,White" will not convert those colors to black and white.

The better technique in that case is to extract a greyscale image of the channel with the highest differences (such as red) and level or normalize that channel.

WARNING: watch out for 'transparent' colors.


The plus form of the operator "+level-colors" on the other hand is extremely useful as it will map the 'black' and 'white' color to the given values compressing all the other colors linearly to fit the two colors you give.

For example lets map 'black' and 'white' to 'green', and 'gold'...

  convert  test.png  +level-colors green,gold   levelc_grn-gold.png
[IM Output] ==> [IM Output]

As you can see the grayscale gradient is remapped into a gradient bound by the colors given, and although colors outside a gray-scale range are also modified, they will also follow the basic style of the color range specified. This makes the "+level-colors" operator an extremely useful one, especially when mapping grayscale images.

If you only supply one colorname but include a comma, the missing color will default either to 'black' or 'white' as appropriate.

  convert test.png  +level-colors ,DodgerBlue   levelc_dodger.png
  convert test.png  +level-colors ,Gold         levelc_gold.png
  convert test.png  +level-colors ,Lime         levelc_lime.png
  convert test.png  +level-colors ,Red          levelc_red.png

  convert test.png  +level-colors Navy,         levelc_navy.png
  convert test.png  +level-colors DarkGreen,    levelc_darkgreen.png
  convert test.png  +level-colors Firebrick,    levelc_firebrick.png
[IM Output] [IM Output] [IM Output] [IM Output]
[IM Output] [IM Output] [IM Output]

This makes it easy to convert grayscale images into a gradient for any color you like. For example here I remap a black and white gradient to a red and white gradient, (note the ',' in the argument)...

  convert cow.gif   +level-colors red,   cow_red.gif
[IM Output] ==> [IM Output]

This didn't just replace 'black' with 'red' but also re-mapped all the anti-aliased gray colors to an appropriate mix of 'red' and 'white', producing a very smooth result. [IM Output]

If I had just performed a Direct Color Replacement converting pure black colors to red, I would end up with the horrible image showing to the right. See Fuzz Factor for the code used to generate that image.

Of course if you want one of the colors to be made transparent instead you are better off using the -alpha Shape operator instead, as this requires you to transfer the gradient into the alpha channel.

If you only specify a single color without any 'comma' separator, that color will be used for both black and white points. That means all the colors in the image will be reset to that one color. (according to the current "-channel" setting limitations).

  convert  test.png  +level-colors dodgerblue  levelc_blue.png
[IM Output]

If you want to set the images transparency setting as well you will need to set "-channel" to include the transparency channel, OR set the Alpha Channel to fully-opaque, using either "-alpha opaque" or "-alpha off.

  convert  test.png -channel ALL +level-colors dodgerblue levelc_blue2.png
[IM Output]

Also see Blanking Existing Images.

Here are a few more examples of using this to adjust or 'tint' a colorful image, rather than a gray-scale image.

  convert rose: +level-colors             navy,lemonchiffon  levelc_faded.gif
  convert rose: +level-colors        firebrick,yellow        levelc_fire.gif
  convert rose: +level-colors 'rgb(102,75,25)',lemonchiffon  levelc_tan.gif
[IM Output] ==> [IM Output] [IM Output] [IM Output]

In summary the "+level-colors" is a gradient color replacement, a linear tinting operator, and can also completely reset colors.

Sigmoidal Non-linearity Contrast

From a PDF paper on 'Fundamentals of Image Processing' (page 44) they present an alternative from the linear contrast control with gamma correction known as 'sigmoidal non-linearity contrast control'.

The result is a non-linear, smooth contrast change (a 'Sigmoidal Function' in mathematical terms) over the whole color range, preserving the white and black colors, much better for photo color adjustments.

The exact formula from the paper is very complex, and even has a mistake, but essentially requires with two adjustment values. A threshold level for the contrast function to center on (typically '50%'), and a contrast factor ('10 being very high, and '0.5' very low).

For those interested, the corrected formula for the 'sigmoidal non-linearity contrast control' is...
(1/(1+exp(β(α-u))) - 1/(1+exp(β))) / (1/(1+exp(β(α-u))/(1+exp(β))))
Where α is the threshold level, and β the contrast factor to be applied.

The formula is actually very simple exponential curve, with the bulk of the above formula is designed to ensure that 0 remains 0 and 1 remains one. That is, the graph always goes though the points 0,0 and 1,1. And the highest gradient of change is at the given threshold.

Here for example is a "-fx" implementation of the above formula, resulting from a very high contrast value of '10' and a '50%' threshold value. These values have been rolled into the floating point constants, to speed up the function.

  convert test.png  -fx '(1/(1+exp(10*(.5-u)))-0.0066928509)*1.0092503' \
              sigmoidal.png
[IM Output] ==> [IM Graph] ==> [IM Output]

Lucky for us IM v6.2.1 had this complex function built in as a new operator "-sigmoidal-contrast", allowing a much simpler application.

    convert test.png  -sigmoidal-contrast 10,50% test_sigmoidal.png
[IM Output]

As a bonus IM also provides the inverse, a 'sigmoidal contrast reduction' function (as plus '+' form of the operator), which if applied with the same arguments restores our original image (almost exactly).

    convert test_sigmoidal.png +sigmoidal-contrast 10,50% \
                                             test_sigmoidal_inv.png
[IM Output]

And here we apply it to the rose image...

    convert  rose:  -sigmoidal-contrast 10,50%  rose_sigmoidal.gif
[IM Output]

I did say '10' was a very heavy contrast factor. In fact anything higher than this value can be considered to be more like a fuzzy threshold operation, rather than a contrast enhancement.

For a practical example of using this operator see the advanced "Gel" Effects Example, where it is used to sharpen the bright area being added to a shaped area color.

Miscellaneous Contrast Operators

Under Construction

   -contrast  and   +contrast
         Rather useless minor contrast adjustment operator

-threshold
   Threshold the image, any value less than or equal to the given value is
   set to 0 and anything greater is set to the maximum value.

   Note that like level, this is a channel operator, but if the default
   'channel setting' is used only the gray-scale intensity of the image is
   thresholded producing a black and white image.

   convert rose: -threshold 45%  x:

   You can force normal channel behaviour, where each channel is thresholded
   individually buy using "-channel All"

   convert rose: -channel All -threshold 45%  x:

-black-threshold
-white-threshold
   This is like -threshold except that only one side of the threshold value is
   actually modified.

   For example, here anything that is darker than 30% is set to black.

   convert rose: -black-threshold 30%  x:
   convert rose: -white-threshold 50%  x:

   These operators however do not seem to be channel effected, so may only be
   suitable for gray-scale images!


Adjustments Using Histogram Modification

This section was a joint effort by Fred Weinhaus and Anthony Thyssen.

What is a histogram?

A histogram is a special type of graph. It simply sorts the color levels of the pixels in an image into a fixed number of 'bins' each of which span some small range of values. As such each bin contains a count of the number of color levels (pixel values) in the image that fall into that range.

The result is a representation of how the color values that make up an image are distributed, from black at the left, to white at at the right.

The histogram can be generated for each channel separately or as a global histogram which looks at values from all the channels combined. The result is often displayed as a image of a bar chart. In IM, this is done using the special Histogram: output format. For example...

  convert rose: histogram:histogram.gif
[IM Output]

But it can also be displayed as a line graph where the line connects the tops of the bars. This will be demonstrated later in the discussion below.

See Histogram: for more details of this special output format. This is recommended reading at this point as it is the best way to extract histogram information about images using IM.

A histogram chart's actual height has little actual meaning, since it is usually scaled so that the highest peak touches the top of the image. As such the height of each individual 'bar' is not relevant. What is much more important is the distribution of the histogram over the whole range, and how the relative heights relate to each other over the whole of the chart.

When looking at a histogram you would consider the following factors.

Essentially a histogram is a simpler representation of an image, and as such it is much easier to change or adjust an image in terms of its histogram.

Almost any mathematical color transformation that one applies to an image will normally cause not only the image to be modified, but its histogram as well. These include linear operations such as the Level Operator or non-linear operations such as the Gamma Operator, (see above). The mapping graphs we saw above represent how the graylevels in an image and thus how the image's histogram is to be transformed.

For example, lets make a low contrast image to demonstrate. However, the final result is that it not only modifies the image, but does so by modifying the image's histogram (by compressing it).

  convert chinese_chess.jpg -contrast -contrast -contrast -contrast \
          chinese_contrast.png

  convert chinese_chess.jpg     histogram:chinese_chess_hist.gif
  convert chinese_contrast.png  histogram:chinese_contrast_hist.gif
[IM Output] ==> [IM Output]
[IM Output] ==> [IM Output]

In the above case, "-contrast, is a simple Level type operator that adds just a little more contrast to the image. the result of this is that the histogram itself is spread out more, causing it to cover the whole of the possible color range better.

You can also see from the histograms, before and after, that the colors will also end up with gaps and holes between the 'bins', due to the way in which the stretching was performed. Specifically it creates a 'histogram' with all the colors being places into 'bin'. These 'binned' colors are then modified as a whole, causing the image colors to be grouped together. It is not a particularly good way of handling image colors.

This operator however works blindly, without any knowledge of the image content or color distribution. It thus cannot be done without some user control, as the operator could very easily make any image it is applied to, worse, rather than better.

In this section we will look at image processing operators that examine the image's histogram as part of its decision making process. It then modifies images using the result this study, so as to enhance some quality of the image color distribution. As these operators make use of actual information coming from the image being processed, they can often be used more globally over many images with much checking by the user.

Operators of this type include automatic linear 'level' type operators such as "-normalize", "-contrast-stretch", and "-linear-stretch", but also non-linear ones such as "-equalize", and others that may eventually be included into ImageMagick such as Fred Weinhaus's script, "redist".

Histogram Stretching

The simplest techniques, like the previous example simply stretch the histogram of the image outward to improve the color range. However instead of just blindly picking the black-point and white-point for Level operation, they select points based on the images histogram.

Basically they count up the number of color values in each histogram bin, from each of the two ends, inward until they reach some threshold. These points will then be used as the black-point and white-point for the histogram (level) stretching.

Diagram needed

Basically the histogram counts provide the graylevel values that the stretch will force to black and white. This means that all pixels in the image that fall within the range of bins from pure black to the selected black-point bin's corresponding graylevel will end up pure black.

Likewise those pixels in the image that fall within the range of bins from from pure white to the white-point bin's corresponding graylevel will end up pure white.

The pixels that are outside these points however will have been stretched outside the possible color range of values, and as a result they will be simply be set to the range limits. That is these pixels are 'clipped' 'burned-in' as they are converted to the extreme of pure black or pure white color values.

As a result if the 'threshold' limits for selecting the black-point and white-point is set too high, you will get lots of black and white areas in the image, with the resulting histogram having large counts (tall bars) at the extreme end bins.

Example of severe burn-in -- Chinese Chess Image?

Normalize

The "-normalize" operator is the simplest of these three operators. It simply expands the grayscale histogram so that it occupies the full dynamic range of gray values, while clipping or burning 2% on the low (black) end and 1% on the on the high (white) end of the histogram. That is, 2% of the darkest grays in the image will become black and 1% of the lightest grays will become white.

This is not a large loss in most images, and the overall result is that the contrast (intensity range) of the image will be automatically maximized.

A idealized diagram is needed here!

Example using chinese chess?

Here we create a gray-scale gradient, and expand it to the full black and white range.

  convert -size 150x100 gradient:gray70-gray30 gray_range.jpg
  convert gray_range.jpg  -normalize  normalize_gray.jpg
[IM Output] ==> [IM Output]

For practical reasons to do with JPEG color inaccuracies (see JPEG Color Distortion for more details) and scanned image noise, "-normalize" does not expand the very brightest and darkest colors, but a little beyond those values. That is, it is equivalent to a "-contrast-stretch" with a value of '2%,99%' (see below).

This means if highest and lowest color values are very close together, "-normalize" will fail, an no action will be taken.

If you really want to expand the exact brightest and darkest color values to their extremes use "-contrast-stretch" with a value of '0' instead.

Up until IM version 6.2.5-5, "-normalize" worked purely as a grayscale operator. That is, each of the red, green, blue, and alpha channels were expanded independently of each other according to the "-channel" setting. As of IM version 6.2.5-5, if only the default "+channel" setting is given, then "-normalize" will tie together all the color channels, and normalizes them all by the same amount. This ensures that pixel colors within the image are not shifted. However, it also means that you may not get a pure white or black color pixel.

For example here we added some extra colors (a blue to navy gradient) to our normalization test image.

  convert -size 100x100 gradient:gray70-gray30 \
          -size  50x100 gradient:blue-navy  +append  color_range.jpg
  convert color_range.jpg -normalize  normalize.jpg
[IM Output] ==> [IM Output]

As you can see from the last example, for color images "-normalize" maximized all the channels together so one channel has a zero value, and another channel has a maximum value. That is, no black pixels were generated, as all the added blue colors already contain 'zero' values in the 'red' and 'green' channels. As such the lower bounds of the image did not expand.

If you want the old "-normalize" behaviour (before IM v6.2.5-5), you will need to specify any non-default "-channel" setting. For images that contain no alpha (or matte) channel, you can just use the 'all' channel setting.

  convert color_range.jpg -channel all  -normalize   normalize_all.jpg
[IM Output]

Alternatively, you can normalize each channel as a separate image using the "-separate" operator (as of IM v6.2.9-2), then "-combine" them back into a single image again.

  convert color_range.jpg -separate -normalize -combine normalize_sep.jpg
[IM Output]

In these last two examples, we see that the grayscale areas of the image turned yellow, since the 'red' and 'green' channels were lightened, while the 'blue' channel is only darkened slightly.

This brings use to an important point
Normalise and other Histogram operators are really grayscale operators,
caution is needed when using it with color images.

In actual fact, "-normalize" is just a subset of the more general "-contrast-stretch" with default values for black-point 2% and white-point=1%. So what is "-contrast-stretch"?

contrast-stretch

The "-contrast-stretch" operator (added IM v6.2.6) is similar to "-normalize", except it allows the user to specify the number of pixels that will be clipped or burned-in. That is it provides you with some control over its selection of the 'black-point' and 'white-point' it will use for the histogram stretching. Thus the user specifies a count (or percent counts) of the darkest grays in the image become black and the count of the lightest greys to become white.

For example, this will replace both the top and bottom 15% of colors with their extremes (white and black), stretching the rest of the 70% of colors appropriately. The final result is to try to improve the overall contrast of the image.

  convert gray_range.jpg  -contrast-stretch 15%  stretch_gray.jpg
[IM Output] ==> [IM Output]

You can also easily see the 'burn' and 'clip' effects at the top and bottom of the above gradient, as those gray colors get stretched well beyond the limits of the color range.

And here I purposefully 'burn' 90% of the darker grays, leaving just 10% of the brightest pixels to be stretched into a tight linear gradient at the top of the image.

  convert gray_range.jpg  -contrast-stretch 90%x0%  stretch_black.jpg
[IM Output]

This can be quite useful in order to find the brightest 'N' pixels in an image, as they will be the only ones not 'burned' to a value of zero.

One important aspect of "-contrast-stretch" is the use of zero for the black-point and white-point threshold counts. In this case, "-contast-stretch 0", will locate the minimum and maximum bins in the image's histogram. Since the counts actually begins at these bins, the result is simply to stretch the min and max bins to full black and full white. This will result in a contrast stretch with a minimum or possibly zero amount of clipping, with all the values in those 'bins' becoming 0 and maximum values.

 
Under Construction

Linear-Stretch

In many ways "-linear-stretch" is very similar to the previous "-contrast-stretch" operator. Both functions can take black-point and white-point arguments as either raw counts or as percentages of the total number of pixels involved. However there are several important differences.

One difference has to do with how the default black-point and white-point is computed. With "-contrast-stretch". If only one value, the black-point, is provided, then the white point will be the same value. Thus "-contrast-stretch 1" is equivalent to "-contrast-stretch 1x1" and "-contrast-stretch 1%" is equivalent to "-contrast-stretch 1x1%"". However, with "-linear-stretch", if only one value, the black-point, is provided, then the white point will be the complement value.

That is, if the black-point is specified as a raw count, then the white-point will be the total pixels in the image minus the black-point count. Likewise, if the black-point is specified as a percent count, then the white-point will be, 100% minus the black-point percentage count. Thus "-linear-stretch 1%" will be equivalent to "-linear-stretch 1x99%".

The second difference has to do with where counts begin. Consider a histogram with 256 bins (some 'bins' which may have zero counts) going from graylevel 0 to graylevel 255. In "-contrast-stretch", counts start at zero with the lowest (min) and highest (max) populated bins in the image (which may or may not be at bin 0 or bin 255 in the histogram). Thus a black-point of 10% will cumulate counts from all bins after the min bin until it reaches 10% and stretch the black side from that graylevel. Thus the amount burned-in at the black side of the histogram will end up being 10% plus what was already found in the darker 'bins' before it. Likewise with counting from the bright side of the histogram.

With "-linear-stretch", the count starts at the ends of the histogram, namely, at bin 0 and bin 255. Thus the amount burned-in at the dark side will always be the black-point value and the amount of burn-in at the bright side will always be the white-point value.

As an example, lets take a gradient of 100 pixels and look at its histogram.


  convert -size 1x100 gradient: \
          -depth 8 -format "%c" histogram:info:
[IM Text]

As expected every bin is equally populated with a single pixel, producing a count of 1. (To see the full listing click on the output text image above).

Now lets do the same after using "-contrast-stretch 10x10%"

  convert -size 1x100 gradient:   -contrast-stretch 10x10%  \
          -depth 8 -format "%c" histogram:info:
[IM Text]

And now "-linear-stretch 10x10%".

  convert -size 1x100 gradient:   -linear-stretch 10x10%  \
          -depth 8 -format "%c" histogram:info:
[IM Text]

So we confirm that for "-contrast-stretch 10x10%" we get 11 pixels at each end. That is equivalent to the count in the end bins plus 10% of the image pixels, which is equal to 10 pixels. So 10+1=11 pixels burned-in. On the other hand, in "-linear-stretch", the end bins end up containing containing only 10 pixels or 10% of the image.

One consequence of the aforementioned difference is that "-contrast-stretch 0x0" may change the image, if the lowest and/or highest populated bins are not the end bins at 0 and 255. In this case, the image will be stretched between the graylevels corresponding to those bins. On the other hand, "-linear-stretch 0x0" will never change the image.

For example, lets take the gradient and compress its graylevels by 10% on each end. That is, we will move black-point up 10% to graylevel 26 and white-point down 10% to graylevel 230.


  convert -size 1x100 gradient:   +level 10x90%  \
          -depth 8 -format "%c" histogram:info:
[IM Text]

Now, lets apply "-contrast-stretch 0x0" to the above de-contrasted gradient


  convert -size 1x100 gradient: -level 10x90%  -contrast-stretch 0x0  \
          -depth 8 -format "%c" histogram:info:
[IM Text]

And now "-linear-stretch 0x0"

  convert -size 1x100 gradient: -level 10x90%  -linear-stretch 10x10% \
          -depth 8 -format "%c" histogram:info:
[IM Text]

So we see that the original image had a histogram that did not span the full dynamic range of 0 to 255. It only went between graylevels 26 and 230. But after applying "-contrast-stretch 0x0", it was stretched to full dynamic range. On the other hand, "-linear-stretch 0x0" made no change in the resulting histogram.

The third difference is that "-contrast-stretch" is channel sensitive, whereas "-linear-stretch" is not.

That means that with "-contrast-stretch" any one or more channels can be changed without affecting the others. Thus if no channel is specified, the overall histogram from all the channels will be used to modify all the channels in the same manner so that no color shifts are produced.

However, if "-channel RGB" is specified, then each channel will be stretched separately and the result will depend upon the end bins in each channel. If they are different, then a color shift will be produced between the individual channels in the resulting image.

With "-linear-stretch", all the channels will be processed in a common way, thus assuring that no color shifts of the channels relative to each other will be produced.

So lets get a verbose identify and the histogram of a real image.

  convert port.png  -verbose -identify +verbose  histogram:port_hist.gif
[IM Text] [IM Output]
[IM Output]

We see that none of the channels of the above image span the full dynamic range. Also note that each of the channels spans a uniquely different range of values.

Now lets apply "-contrast-stretch 1x1%" without a "-channel" setting.

  convert port.png -contrast-stretch 1x1% \
          -write histogram:port_cs1_hist.gif   port_cs1.png
[IM Output] [IM Output]

In the above result, the image is stretched consistently across all the channels. Thus, there are no color shifts between channels. Now let's do the same but with "-channel RGB".


  convert port.png  -channel RGB  -contrast-stretch 1x1% \
          -write histogram:port_cs1rgb_hist.gif    port_cs1rgb.png
[IM Output] [IM Output]

In the above result, because we set "-channel RGB", rather than use the default channel setting, the image is stretched differently for each channel. This causes a color shift between channels.

Now let's apply "-linear-stretch" without a "-channel" setting.


  convert port.png   -linear-stretch 1x1% \
          -write histogram:port_ls1_hist.gif \
          port_ls1.png
[IM Output] [IM Output]

In the above result, the image is stretched consistently across all the channels. So there is no color shift between channels. Now let's do the same, but with "-channel RGB".

  convert port.png  -channel RGB  -linear-stretch 1x1% \
          -write histogram:port_ls1rgb_hist.gif    port_ls1rgb.png
[IM Output] [IM Output]

In the above result with "-linear-stretch", the image is stretched consistently across all the channels and "-channel RGB" is ignored. Thus there is no color shift between channels and the result is identical to that above without "-channel RGB".

 

Histogram Redistribution

Histogram redistribution is a non-linear technique that redistributes the bins in a histogram in order to achieve some particular shape. The two most common shapes are uniform (flat) and Gaussian (bell-shaped), although Hyperbolic and Rayleigh are other types of distributions have also been used.

Equalize - Uniform Histogram Redistribution

For the case of a uniform distribution, the histogram bins are shifted, spaced and combined so that on average the histogram has a flat or constant height across the whole range. This is called histogram equalization. The IM function, "-equalize", does this.

Unfortunately, it operates on each channel separately, rather than applying the same operation to all channels. As such, color shifts are possible, when it is applied to RGB colorspace.

Here is an example of histogram equalization using the IM function -equalize. Notice the color balance shift from the equalization on each channel independently.

  convert zelda.png  -write histogram:zelda_hist.gif \
          -equalize  -write histogram:zelda_equal_hist.gif \
          zelda_equal.png
[IM Output] ==> [IM Output]
[IM Output] ==> [IM Output]

You may note that the histogram does not look very uniform. But if we convert the resulting image to grayscale and display its histogram, its histogram looks a bit more uniform in comparison to the original image's grayscale histogram


  convert zelda.png  -colorspace gray   histogram:zelda_ghist.gif

  convert zelda_equal.png  -colorspace gray \
          histogram:zelda_equal_ghist.gif
[IM Output] ==> [IM Output]

The other way to approach redistributing the bins is by using a transformation look up table that is generated from the separate cumulative histograms of each channel and the desired integrated distribution curve. If one does not want any color shifts between channels, then one uses the combined histogram from all the channels of the image. An approximation is simply to use the histogram of the image after converting it to grayscale.

Fred Weinhaus has developed a script, called "redist" that does just that. It redistributes the histogram of an image into a uniform or a Gaussian distribution.

  redist -s uniform zelda.png  zelda_uniform.png

  convert zelda_uniform.png   histogram:zelda_uniform_hist.gif
[IM Output] ==> [IM Output]
[IM Output] ==> [IM Output]

Note how the results different from the IM built-in "-equalize" operator. Specifically, all the colors are preserved, without the color shift you saw previously.

What the script does is work on the grayscale histogram, which it then applies to all the color channels, so that all the colors are kept together.

For comparison with the IM "-equalize" histograms, lets show the grayscale histogram results here, too. Note that the redistributed histogram appears to be a bit more leveled out (flat, or uniform) than that of IM equalize.


  convert zelda.png  -colorspace gray   histogram:zelda_ghist.gif

  convert zelda_uniform.png  -colorspace gray \
          histogram:zelda_uniform_ghist.gif
[IM Output] ==> [IM Output]

Equalize in other colorspaces! That is, the grayscale channel in HSL, HSB and CMYK colorspaces.

Gaussian Redistribution

Equalizing a histogram is not the only way of changing the histogram distribution of an image. Actually it isn't normally very useful, except in computer vision applications.

Here is the same image, but transformed so its histogram has a Gaussian (bell-shaped) distribution. The values used here are a 60% gray mean, with a 60 sigma roll-off to either side of that mean.

  redist -s gaussian 60,60,60  zelda.png \
         zelda_gaussian.png

  convert zelda_gaussian.png -colorspace gray \
          histogram:zelda_gaussian_ghist.gif
[IM Output] ==> [IM Output]
[IM Output] ==> [IM Output]

From the resulting grayscale histogram, you can see that the image is modified so its colors follow a Gaussian bell curve type of distribution.

For photos, this produces a more 'natural' looking result. The image will not only have been contrast optimized, but also adjusted in brightness so most of the pixels in the image have about a 60% grayscale brightness.

Histogram Redistribution Methodology

So how does this type of direct histogram adjustment work?

Basically it computes the histogram of the current image and that of the desired distribution. It then works out how the graylevel value of each 'bin' needs to be changed so that the counts in the bins best follow the desired distribution. Some bins may be shifted darker, while others may be shifted lighter.

This is actually quite an involved process, so lets go though it step by step.


First, we need to get the actual histogram data from ImageMagick, rather than a graphic image of the histogram. Note that the data is from all the color values, combined into a grayscale. This was done so as to distribute all the channels together, and adjust the image overall brightness to follow to the desired curve.

  convert zelda.png -colorspace gray \
         -depth 8 -format "%c" histogram:info:- |\
    tr -cs '0-9\012' ' ' |\
      awk '# collect the histogram data.
           { bin[$2] += $1; }
           END { for ( i=0; i<256; i++ ) {
                   print bin[i]+0;
                 }
               } ' > zelda_hist_data.txt

  # get the maximum count for any one histogram 'bin'
  max_count=`sort -n zelda_hist_data.txt | tail -n 1`

  # convert histogram into a profile graph of the data
  echo "P2 256 1 $max_count" | cat - zelda_hist_data.txt |\
    im_profile -s - zelda_hist_graph.gif
[IM Output] ==> [IM Output]

To collect the data I take the 'comment' meta-data from the histogram image, which IM includes just for this purpose. The data is then cleaned to leave just the raw numbers (using a program called "tr", short for 'translate'). This raw data is then given to another utility program called "awk", which is used to collect the actual histogram counts for each bin.

So that we can look at the results, I also process the histogram counts into a gradient image (via the NetPBM, PGM text grayscale image file format, and display it as a line graph using the "im_profile" script. Essentially this is just a different way of generating a histogram image, though this time directly from a numerical data file.

Now that we have the histogram data in a text file, we also need the histogram of the function we want the redistributed data to match. In this case, it is a Gaussian distribution with a mean value of 153 (60% gray) and sigma width of 60. Both values are in terms of the 256 range of the histogram 'bins'.

  awk '# AWK to generate gaussian distribution graph
        BEGIN { mean = 153;   sigma = 60;
                fact = 1/(2*(sigma/256)^2);
                expo = exp(1);
                for ( i=0; i<256; i++ ) {
                  print int(65535*expo^(-(((i-mean)/256)^2)*fact));
                }
              }' /dev/null  > gaussian_hist_data.txt

   # convert gaussian data into a profile graph
   echo "P2 256 1 65535" | cat - gaussian_hist_data.txt |\
     im_profile -s -b - gaussian_hist_graph.gif
[IM Output]

The histograms above are interesting and reflect the image's original histogram distribution and the histogram's desired state. But for conversion purposes, this form of histogram, while good for us to understand, is not very useful for our purposes.

Actually, what we really need are the cumulative histograms. These histograms are very similar to a normal histogram, except that each 'bin' in the histogram is a count of its 'bin' plus all the 'bins' that came before it, starting at 0. That is, each 'bin' is an 'accumulation' or count of all the darker 'bins'.

These are actually easier to generate directly from the original image. So lets repeat the process, but computing and saving the 'cumulative' counts.

  convert zelda.png -colorspace gray \
         -depth 8 -format "%c" histogram:info:- |\
    tr -cs '0-9\012' ' ' |\
      awk '# Collect the cumulative histogram for an image
               { bin[$2] += $1; }
           END { for ( i=0; i<256; i++ ) {
                   cum += bin[i];
                   print cum;
                 }
               } ' > zelda_cumhist_data.txt

  total_count=`tail -n 1 zelda_cumhist_data.txt`
  echo "P2 256 1 $total_count" | cat - zelda_cumhist_data.txt |\
    im_profile -s - zelda_cumhist_graph.gif

  awk '# AWK to generate gaussian distribution cumulative graph
        BEGIN { mean = 153;   sigma = 60;
                fact = 1/(2*(sigma/256)^2);
                expo = exp(1);
                for ( i=0; i<256; i++ ) {
                  gas[i] = expo^(-(((i-mean)/256)^2)*fact);
                  total += gas[i]
                }
                for ( i=0; i<256; i++ ) {
                  cum += gas[i];
                  print int(65535*cum/total);
                }
              }' /dev/null  > gaussian_cumhist_data.txt

  total_count=`tail -n 1 gaussian_cumhist_data.txt`
  echo "P2 256 1 $total_count" | cat - gaussian_cumhist_data.txt |\
    im_profile -s -b - gaussian_cumhist_graph.gif
[IM Output]
Image Cumulative
Histogram
[IM Output]
Gaussian Cumulative
Histogram

Now what we need to do is convert the image's cumulative histogram into the gaussian cumulative histogram. To do this, each gray value in the input image is used to find its 'normalized' cumulative value. This is then mapped to the same cumulative value in the gaussian distribution and then its corresponding gray value is found.

This diagram should make the mapping process clearer...
[diagram]

The following command does the lookup for every possible 8-bit color value, in order to generate a Color Look Up Table, or CLUT. This special image can then be used to map the color values in the original image to the new values needed to redistribute the image's histogram.

  # Generate a CLUT to Redistribute the Histogram
  paste  zelda_cumhist_data.txt   gaussian_cumhist_data.txt |\
    awk '# AWK to generate gaussian distribution graph
              { bin[NR] = $1;   gas[NR] = $2;  }
          END { k=0;  # number of pixels less than this value
                print "P2 256 1 65535";
                for ( j=0; j<256; j++ ) {
                  while ( k<255 &&
                            gas[k]/gas[255] <= bin[j]/bin[255] ) {
                    k++;
                  }
                  print 65535*k/255;
                }
              }' |\
      convert pgm:- gaussian_clut.png

  convert zelda.png   gaussian_clut.png -clut   zelda_redist.png
[IM Output] ==> [IM Output]
[IM Output] ==> [IM Output]

As you can see, converting a histogram of an image to attempt to follow a specific distribution function, such a gaussian bell curve, is quite an involved and highly numerical process.

Here it is all in one rather long and complex command...

  convert zelda.png -colorspace gray \
         -depth 8 -format "%c" histogram:info:- |\
    tr -cs '0-9\012' ' ' |\
      awk '# AWK to generate gaussian distribution graph
            { # just read in image histogram into a 'bin' table
                  bin[$2] += $1;
                }
            END { # Generate Gaussian Histogram
                  mean = 153;   sigma = 60;
                  fact = 1/(2*(sigma/256)^2);
                  expo = exp(1);
                  for ( i=0; i<256; i++ ) {
                    gas[i] = expo^(-(((i-mean)/256)^2)*fact);
                  }
                  # Convert normal histograms to cumulative histograms
                  for ( i=0; i<256; i++ ) {
                    gas[i] += gas[i-1];
                    bin[i] += bin[i-1];
                  }
                 # Generate Redistributed Histogram
                 k=0;  # number of pixels less than this value
                 print "P2 256 1 65535";
                 for ( j=0; j<256; j++ ) {
                   while ( k<255 &&
                            gas[k]/gas[255] <= bin[j]/bin[255] ) {
                     k++;
                   }
                   print 65535*k/255;
                 }
                }' |\
        convert zelda.png   pgm:-  -clut   zelda_gaussian_redist.png
[IM Output] ==> [IM Output]

Just some final words on the above technique.

The above will hopefully eventually be built into ImageMagick. In the mean time Fred Weinhaus's "redist" script is available to do the task.

You may also be interested in Fred's "retinex" script, which attempts to make similar automatic enhancements to images, in localized regions of the image, rather than globally as this technique does.


DIY Level Adjustments

Mathematical Linear Histogram Adjustments

The various basic forms of Level Adjustments shown above linearly adjust the colors of the image.

These changes can be applied mathematically as well. For example by multiplying the image with a specific color, we set all pure white areas to that color. So lets just read in our image, create an image containing the color we want, then multiply the original image with this color using the IM free-form "-fx" or DIY Operator.


  convert test.png  -size 1x1 xc:Yellow \
          -fx 'u*v.p{0,0}'    fx_linear_white.png
[IM Output] ==> [IM Output]

By getting "-fx" to read the color from a second 'v' image makes it easy to change the color, without needing to convert colors to RGB values for use in the mathematics.

If you were using a fancy graphical image processing package like "Gimp" and "Photoshop" the above operation would have been applied to an image by adjusting the images color histogram graph 'curve'.

[IM Output] For example to the right is a "gnuplot" generated graph (See the script "im_histogram") of the mathematical formula showing what happens to just one of the three RGB channels. The original color (green line) is remapped to a darker color (red line) linearly.

Linearly tinting the black colors is also quite simple. For example to linear map 'black' to a gold like color 'rgb(204,153,51)', (while leaving 'white' as 'white'), would require a mathematical formula such as...
          result = 1-(1-color)*(1-intensity)
This formula negates the colors, multiples the image with the negated color wanted, and negates the image back again. The result is tinting of the black side of the gray scale, leaving white unchanged.


  convert test.png  -size 1x1 xc:'rgb(204,153,51)'  \
          -fx '1-(1-v.p{0,0})*(1-u)'   fx_linear_black.png
[IM Output] ==> [IM Output] ==> [IM Output]

A "gnuplot" histogram graph of the remapping formula is also displayed in the above for your reference.

With a slightly more complicated formula you can linearly replace both the 'black' and 'white' end of the grayscale with specific colors.

  convert test.png  -size 1x2  gradient:gold-firebrick \
          -fx 'v.p{0,0}*u+v.p{0,1}*(1-u)'   fx_linear_color.png
[IM Output] ==> [IM Output] ==> [IM Output]

The "-size 1x2 gradient:color1-color2" in the above is only used to generate a two color pixel image for the "-fx" formula to reference. The first color replaces white, while the second replaces black, while all others are interpolated between white and black. As is typical of a gray-scale operator, each RGB channel is treated as a separate gray scale channel, though the linear interpolation is different for each channel.

This by the way is exactly equivalent to the Level Adjustments by Color operator "+level-colors"

However unlike "+level-colors", the colors to use can of course come from any image source, and not just the color names provided as an argument. However even direct use of color names is possible.

  convert test.png   -fx "yellow*u+green*(1-u)"  fx_linear.png
[IM Output]

Mathematical Non-linear Histogram Adjustments

While linear color adjustments are important, and faster methods are available, there are many situations where a linear 'level' adjustment, is not what is wanted, and this is where the "-fx" DIY Operator, becomes more useful.

Well an alternative formula for linear adjustment is "-fx 'v.p{0,1}+(v.p{0,0}-v.p{0,1})*u'", which has the advantage that the 'u' can be replaced by a single random function 'f(u)' to produce non-linear color change.

This lets you do more interesting things. For example what if in the last example you wanted to push all the colors toward the 'black' side, resulting in the image being a more 'firebrick' color.

  convert test.png -size 1x2  gradient:gold-firebrick \
          -fx 'v.p{0,1}+(v.p{0,0}-v.p{0,1})*u^4'  fx_non-linear.png
[IM Output] ==> [IM Output] ==> [IM Output]

In a more practical example, Adelmo Gomes needed a color adjustment for a automated Weather Map Recoloring script he was developing.

In this case he wanted to tint pure black parts of the image to a .25 blue, but leave the rest of the gray-scale alone, especially the white and mid-tone grays of the image. Only the blue color needed such adjustment, which he currently was doing by hand in an image editor.

For example you could use a quadratic formula like 'u^2' to tint the black end of the histogram to a '.25' blue color. Only the blue channel needs to be modified, so the value was inserted directly into the formula.

  convert test.png  -channel B  -fx '.25+(1-.25)*u^2'  fx_quadratic.png
[IM Output] ==> [IM Output] ==> [IM Output]

However while this produced a reasonable result it does darken the mid-tone grays slightly, producing a sickly off-yellow color.

To avoid this a 'exponential' function can be used instead, to give better control of the tinting process.

  convert test.png  -channel B  -fx '.3*exp(-u*4.9)+u'  fx_expotential.png
[IM Output] ==> [IM Output] ==> [IM Output]

Again the graph show how blue channel was modified to give black a distinctive dark blue tint.

The second value ('4.9') is the falloff back to a linear '+u' graph. The smaller this value is the slower the fall off, and the more linear the adjustment becomes. The larger the value, the more dramatic the 'fall-off'. The value may need to be adjusted for different color values, so this is not a good general formula for general black color tinting, but perfect for tinting weather maps.

Generally if you can express the color adjustment you want mathematically, you can then use "-fx" operator to achieve the results you want.

'Curves' Adjustments

[diagram] Normally in a graphical photo editor you would be presented with a histogram 'curves' chart such as I have shown to the left. The user can then edit the 'curve' by moving four (or more) control points, and the histogram adjustment function will follow those points.

The control points generally specify that the first grayscale level is after adjustment to become the second grayscale level. So a point like 0.0,0.2 basically means that a 0% gray (black) should after adjustment be a 20% gray level.

Now IM does not allow you to directly specify 'control points' to generate a 'curve' adjustment, what it wants is the mathematical formula of that 'curve' generated. Lucky for us there are programs that can generate that curve formula from the control points, including "gnuplot", "fudgit", "mathematica", and "matlab", as well as many more mathematical software packages.

The following is one method you can use to generate the formula from four control points using "gnuplot" which is a standard extra package you can install on most linux distributions (and is available for Windows too)...

  ( echo "0.0 0.2";  echo "1.0 0.9"; \
    echo "0.2 0.8";  echo "0.7 0.5"; )   > fx_control.txt

  ( echo 'f(x) = a*x**3 + b*x**2 + c*x + d'; \
    echo 'fit f(x) "fx_control.txt" via a, b, c, d'; \
    echo 'print a,"*u^3 + ",b,"*u^2 + ",c,"*u + ",d'; \
  ) | gnuplot 2>&1 | tail -1             > fx_funct.txt
[Data]
Control Points
==> [Gnuplot]
[Gnuplot]
Gnuplot Fitted FX Function

Note that the number of parameters ('a' to 'd' in above) needed for curve fitting, must equal the number of control points you provide. As such if you want five control points you need to include another 'e' term to the function.

If your histogram curve goes though the fixed control points 0,0 and 1,1, you really only need two parameters as 'd' will be equal to '0', and 'c' will be equal to '1-a-b'.

As you can see from the extra "gnuplot" generated image above, the function generated fits the control points perfectly. Also as it generated a "-fx" style formula it can be used as is as an IM argument.

For example...

  convert test.png    -fx "`cat fx_funct.txt`"     fx_funct_curve.png
[IM Output]

To make it easier for users to convert control points into a histogram adjustment function, I have created a shell script called "im_fx_curves" to call "gnuplot", and output a nicer looking polynomial equation of the given the control points. Gabe Schaffer, also provided a perl version (using a downloaded "Math::Polynomial" library module) called "im_fx_curves.pl" to do the same thing. Either script can be used.

For example here is a different curve with 5 control points...

    im_fx_curves  0,0.2  0.3,0.7  0.6,0.5  0.8,0.8  1,0.6  > fx_curve.txt
[Gnuplot] ==>
[Gnuplot]

However the FX function is very slow. But as of IM 6.4.8-9 you can now directly pass the discovered coefficients of the fitted polynomial expression directly into a Polynomial Function Method.

You can generate the comma separated list of coefficients using "im_fx_curves" with a special '-c' option...

    im_fx_curves -c  0,0.2  0.3,0.7  0.6,0.5  0.8,0.8  1,0.6  > coefficients.txt
[Gnuplot] ==>
[Gnuplot]

For example lets apply those curves to our test image...

  convert test.png  -function Polynomial `cat coefficients.txt`  test_curves.png
[IM Output]

A more practical example of this method is detailed in the advanced "Aqua" Effects example.


Tinting Images

Uniformly Color Tinting Images

Typically tinting an image is achieved by blending the image with a color by a certain amount. This can be done using an Evaluate Operator or Blend Images techniques, but these are not simple to use.

Lucky for us a simpler method of bleeding a uniform color into an image is available by using the "-colorize" image operator. This operator blends the current "-fill" color, into all the images in the current image sequence. The alpha channel of the original image is preserved, with only the color channels being modified.

For example lighten an image (gray scale or otherwise) we use "-colorize" to blend some amount of white into the image, making it brighter without saturating the image completely.

  convert test.png  -fill white -colorize 50%  colorize_lighten.png
[IM Output] ==> [IM Output]

Similarly we can use a 'black' fill color to darken an image.

  convert test.png  -fill black -colorize 50%  colorize_darken.png
[IM Output] ==> [IM Output]

To gray both ends of the image toward the mid-tones, you would use a specific gray fill color. The color 'gray50' is the exact middle color of the RGB color spectrum.

  convert test.png  -fill gray50 -colorize 40%  colorize_grayer.png
[IM Output] ==> [IM Output]

The "-colorize" operator also allows you to specify dissolve percentages for each of the three color channels separately. This is useful for linearly darkening (or lightening) an image in a special way.

One common use of the "-colorize" operator is to simply replace all the colors in an existing image (tinting '100%') to set the color of a shape mask.

  convert test.png -fill blue -colorize 100%   colorize_shape.png
[IM Output]

Or by resetting the alpha channel, make a completely blank canvas..

  convert test.png -fill blue -colorize 100% -alpha opaque colorize_blank.png
[IM Output]

However these functions can be handled faster by using Level Adjustments by Color operator, with a single color. See also Blank Canvases.

Midtone Color Tinting

While a "-colorize" operator applies the "-fill" color to tint all the colors in an image linearly, the "-tint" operator applies the "-fill" color in such a way as to only tint the mid-tone colors of an image.

The operator is a grayscale operator, and the color is moderated or enhanced by the percentage given (0 to 200). To limit its effects it is also adjusted using a mathematical formula so that it will not effect black and white. but have the greatest effect on mid-tone colors of each color channel.

A "-tint 100" essentially will tint a perfect gray color so that it becomes the current fill color. A lower value will tint it to a darker color while a higher value will tint to a lighter shade of that color.


  convert  test.png  -fill red  -tint 40 tint_red.png
[IM Output] ==> [IM Output]

The green color in the test image is not a true RGB green, but a Scaled Vector Graphics 'green', which is only half as bright as a true green color. As such it is also a mid-tone color, and thus is affected by the "-tint" operator, becoming darker, unlike red and blue color spots of the test image.

Also you can tint the individual color components, by using a comma separated list of percentages. For example "-tint 30,40,20,10". This however can be tricky to use and may need some experimentation to get right. Better to specify the color you want for perfect 50% grays.

[IM Output] The "-tint" operator works by taking the color and percentages given then then adjusting the individual colors in the image according to the "-fill" colors intensity, as per the following formula. (see graph right)

f(x)=(1-(4.0*((x-0.5)*(x-0.5))))

A quadratic function, the result of which is used as vector for the existing color in the image. As you can see gives a complete replacement of the color for a pure mid-gray, with no adjustment for either white or black.

Or lower level operators that you can use to DIY this sort of thing, see FX Operator, as well as Evaluate and Function Operators.

The tinting operator is perfect to adjust the results of the output of "-shade", (See Shade Overlay Highlight Images), such as the examples in 3d Bullet Images.

You can also use "-tint" to brighten or darken the mid-tone colors of an image. This is sort of like a 'gamma adjustment' for images, though not exactly.

For example using a tint value greater than 100 with a 'white' color will brighten the mid-tones.

  convert  test.png  -fill white  -tint 130 tint_lighter.png
[IM Output] ==> [IM Output]

While a value less than 100 will darken colors.

  convert  test.png  -fill white  -tint 70 tint_darker.png
[IM Output] ==> [IM Output]

As "-tint" uses the color as a 'vector' in color space, a "-fill" color of 'black' will have no effect on the result, as it produces a zero color vector.

On the same note tinting with 100% 'white' generates no change in the mid-tone colors of the image. Other colors darken the midtones so that perfect grays will become the color specified.

Sepia Tone Coloring

A special photographic recoloring technique, "-sepia-tone" is basically consists to converting the image into a gray-scale, and coloring all the mid-tones to a special brown color.

  convert rose:   -sepia-tone 65%     sepia-tone.jpg
[IM Output]

The argument given is the gray-scale 'mid-point' that is to become the closest to the sepia-tone color, which is similar to the color 'Goldenrod'.

The most common use of this is to generate a Duotone Effect so as to generate 'old looking' photos (See wikipedia on Sepia Tone).

For example here I Tint a contrast enhanced gray-scale rose image, using various colors, to achieve similar sepia-tone like effects. Which color you should use on the exact effect you are looking for.

  convert rose: -colorspace gray -sigmoidal-contrast 10,40%  rose_grey.jpg
  for color in      goldenrod  gold  khaki  wheat
  do
    convert rose_grey.jpg  -fill $color   -tint 100    sepia_$color.jpg
  done
[IM Output] ==> [IM Output] [IM Output] [IM Output] [IM Output]

I myself find that mixing or blending a sepia-tone image, with the original, so as to reduce its effect can also produce a better 'faded' effect.

  convert rose:  \( +clone -sepia-tone 60% \) -average  sepia-tone_blended.jpg
[IM Output]

See also Hald Color Lookup Tables for a method by which you can save much more complex color change variations, such as the last example above.

Duotone Effect

A 'duotone' is a printing method where you mix the grayscale of an image (black ink) with some other color to produce a better result, with a limited budget or printing equipment. For example the reason all the old photos you see today have a sepia-tone look about them, is because sepia-tone inks survived and did not deteriorate, or fade with time. Other 'black and white' images formats faded into uselessness. See the Sepia Tone Operator above.

Another duotone technique known as 'Cyanotype' (more commonly known as 'blue-prints') became widely used as method of making large scale copies of the original black and white architect drawings. Remember this tenchique was used long before the invention of lazers and from that photo-copying (and Xerox).

For more information see the Wikipedia entry for Duotone, also Fake duotones vs Real duotones.

The above Tint Operator however produces a reasonable facsimile of the duotone effect, just as it did for a sepia-tone like effect above.

  convert rose: -colorspace gray -sigmoidal-contrast 10,40%  rose_grey.jpg
  for color in      blue  darkcyan  goldenrod  firebrick
  do
    convert rose_grey.jpg   -fill $color   -tint 100    duotone_$color.jpg
  done
[IM Output] ==> [IM Output] [IM Output] [IM Output] [IM Output]

Note that I generally chose a darker version of the 'duotone' color, but you can also adjust this using the argument of the Tint Operator. The brightness and contrast can also be adjusted using the arguments of the Sigmoidal Contrast Operator.

Another more exacting way of generating a duotone from three colors (the black-point, mid-point and white-point colors) is to use a Color Lookup Table (see below).

Here is just a quick example where I create a very unusual duotone using the colors 'Black', 'Chocolate', and 'LemonChiffon' for the duotone. And yes the black-point color is typically left black, which is why it is usally called duo-tone.

  convert -size 1x1 xc:Black xc:Chocolate xc:LemonChiffon \
                                   +append     duotone_clut.gif
  convert -size 20x256 gradient: -rotate 90   duotone_clut.gif \
          -interpolate Bicubic -clut       duotone_gradient.gif
  convert rose_grey.jpg   duotone_clut.gif \
          -interpolate Bicubic -clut       rose_duotone.jpg
[IM Output] ==> [IM Output]
[IM Output] ==> [IM Output] ==> [IM Output]

The advantage of the above is an exact control of the mid-point color (unlike Tint which isn't exact). You can also use with the three colors directly, as I did above, or use an expanded gradient of the colors for finer control of the colors between the three (or more) control points.

The technique also provides you with a very compact way of storing the specific duotone effect, for repeated and future usage.

Also see Hald Color Lookup Tables for more complex method of saving color changes, that go beyond coloring greyscale images.

Color Tinting, DIY

One of the biggest problems with "-tint" is that it is a grayscale (or vector) operator. That is, it handles each of the red,green,blue channels completely separately to each other. That in turn means that a primary and secondary color like 'blue' or 'yellow' are not affected by "-tint", even though all the gray levels are.

However thanks to various channel mathematical transforms such as the FX Operator and the faster Evaluate and Function Operators, you can generate your own color overlays to modify the image. That is, to Tint the image in a similar what that the Colorize Operator does.

For example, here I convert an image's gray-scale brightness level into a semi-transparent overlay of the specific color wanted.

  convert test.png  \( +clone -colorspace gray \
               -function polynomial -4,4,0 -background Gold -alpha shape \) \
          -composite   tint_diy_compose.png
[IM Output]

Note that unlike tint, any color can be used, including 'black' as the color is not treated as a vector addition, but an alpha composition. The result is not quite the same as what you would get for a normal tint.

Color Tinting Overlay

The special Alpha Composition methods 'Overlay' and 'Hardlight' were actually designed with color (and pattern) tinting in mind. These compose methods also will replace mid-tone grays leaving black and white highlights in the image alone.

For example here I quickly generate a colored overlay image, and compose it to tint the original image.

  convert test.png \( +clone +matte -fill gold -colorize 100% \) \
          -compose overlay -composite  tint_overlay.png
[IM Output]

As you can see the alpha composition does not preserve any transparency of the original image, requiring the use of a second alpha composition operation to fix this problem.

  convert test.png \
          \( +clone +matte -fill gold -colorize 100% \
             +clone +swap -compose overlay -composite \) \
          -compose SrcIn -composite  tint_overlay_fixed.png
[IM Output]

Using 'Overlay' is much more linear form of tinting than the quadratic function used above, and like "-tint" is applied to each channel of the image separately such that primary and secondary colors are also left unchanged.

Also no adjustment control is provided by this alpha composition method, so if you want to control the level of tinting, you will need to adjust the overlay image transparency before applying the tint.

Of course unlike the other tinting methods I have shown so far, you are not limited to tinting a simple color, but can apply a tint using an image, or tile pattern.

  convert test.png \
          \( -size 150x100 tile:tile_disks.jpg \
             +clone +swap -compose overlay -composite \) \
          -compose SrcIn -composite  tint_overlay_pattern.png
[IM Output]

This however is getting outside the scope of basic color handling so I'll leave image tinting at that.

The alpha composition method 'HardLight' will produce the same results as 'Overlay' but with the source and destination images swapped.

This could have been used instead of the "+swap" in the last few examples.


Global Color Modifiers

Modulate Brightness, Saturation, and Hue

The "-modulate" operator is special in that it modifies an image in the special HSL (hue-saturation-luminance) colorspace. It converts each color pixel in into this color space and modifies it and converts it back to its original color space.

It takes three values (though later values are optional) as a percentage such that 100 will make no change to an image. For example..

  convert  rose:  -modulate 100,100,100  mod_noop.gif
[IM Output]

The first value, brightness is a multiplier of the images overall brightness.

  convert rose:   -modulate 0     mod_bright_0.gif
  convert rose:   -modulate 50    mod_bright_50.gif
  convert rose:   -modulate 80    mod_bright_80.gif
  convert rose:   -modulate 100   mod_bright_100.gif
  convert rose:   -modulate 150   mod_bright_150.gif
  convert rose:   -modulate 200   mod_bright_200.gif
[IM Output] [IM Output] [IM Output] [IM Output] [IM Output] [IM Output]

Note that while a brightness argument of '0' will produce a pure black image, you cannot produce a pure white image using this operator on its own.

The second value saturation is also a multiplier adjusting the overall amount of color that is present in the image.

  convert rose:   -modulate 100,0     mod_sat_0.gif
  convert rose:   -modulate 100,20    mod_sat_20.gif
  convert rose:   -modulate 100,70    mod_sat_70.gif
  convert rose:   -modulate 100,100   mod_sat_100.gif
  convert rose:   -modulate 100,150   mod_sat_150.gif
  convert rose:   -modulate 100,200   mod_sat_200.gif
[IM Output] [IM Output] [IM Output] [IM Output] [IM Output] [IM Output]

A saturation of '0' will produce a grayscale image, as was also shown in Converting Color to Gray-Scale above. The gray however mixes all three color channels equally, as defined by the HSL colorspace, as such does not produce a true 'intensity' grayscale.

Essentially small values produce more 'pastel' colors, while values larger than '100' will produce more cartoon-like colorful images.

Note that as the brightness and saturation are percentage multipliers, you would need to multiply by a very large number to change almost all the image color values to near maximum. That is you would need to use a brightness factor of close to one million, to make all colors except pure black, white.

The final value, Hue, rotate the colors of the image, in a cyclic manner. With '100' producing no change, and '0' or '200' producing a negative hue (not a brightness negation).

  convert rose:   -modulate 100,100,0      mod_hue_0.gif
  convert rose:   -modulate 100,100,33.3   mod_hue_33.gif
  convert rose:   -modulate 100,100,66.6   mod_hue_66.gif
  convert rose:   -modulate 100,100,100    mod_hue_100.gif
  convert rose:   -modulate 100,100,133.3  mod_hue_133.gif
  convert rose:   -modulate 100,100,166.6  mod_hue_166.gif
  convert rose:   -modulate 100,100,200    mod_hue_200.gif
[IM Output] [IM Output] [IM Output] [IM Output] [IM Output] [IM Output] [IM Output]

As you can see a value of '33.3' produces a negative, or counter-clockwise rotation of all the colors by approximately 60 degrees, effectively mapping the red to blue, blue to green, and green to red. Using values of '0' or '200' produces a complete 180 degree negation of the colors, without negating the brightness of the image. A value of '300' will produce 360 degrees in color rotation resulting in no change to the image.

To achieve this the Hue value given produces a 'modulus addition', rather than a multiplication.

These types of operations and more can also be applied using advanced Color Space techniques, such as using in Recolor Matrix Operator (below), but for basic 'modulation' of an image, this operator greatly simplifies things.

For primary color swapping, either Recolor Matrix Operator, or channel swapping (see Separate/Combine Operators), is probably more accurate technique. Though it is much less versatile.

See also Hald Color Lookup Tables for a method by which you can save color change variations, especially changes in Hue, for later reuse.

Modulate DIY

You can if really want to "Do It Yourself". You basically convert the image into the appropriate color space, modify the values, and convert back. All you have to remember that in HSL Color Space, the Green channel holds the Saturation value, and the Blue channel holds the Luminance value.

For example here is the equivalent to a "-modulate 80,120", using the default HSL colorspace...

  convert rose: -colorspace HSL \
          -channel B -evaluate multiply 0.80 \
          -channel G -evaluate multiply 1.20 \
          +channel -colorspace RGB   modulate_channel.png
[IM Output]

Of course if you modify the Hue (red channel) using this method you will need to ensure the final value 'wraps' around (a modulus), rather than simply clipping the value at the maximum or minimum value (both of which is the 'red' hue). As such it is probably easier to just directly use the "-modulate" operator, for Hue modifications, as the colorspace containing the hue does not matter.

Modulate in Other Colorspaces

The biggest problem with "-modulate" is when handing images containing a lot of 'near white' colors. As it does its work in HSL colorspace, colors that are off-white will become more 'saturated' as the brightness is reduced. You can see this in the white leaf of the rose image above, which shows lots of color artifacts at a 50% darkening.

This is especially a problem when dealing with JPEG image formats, as it tends to generate off-white colors (actually all colors are generally slightly off in JPEG) due to its lossy compression algorithm. For example...

  convert wedding_party_sm.jpg  -modulate 85  modulate_off-white.png
[IM Output] ==> [IM Output]

The problem here is that in HSL all the off white colors were packed into a small 'white point' of the color space used. When brightness is then reduced the area off-white color get expanded as the cone of color expands, causing the off white color to generate a more colorful (saturated) set of off-white colors. That is, small variations in color are exaggerated.

The solution to this is to "-modulate" in the HSB colorspace instead of HSL colorspace.

In this colorspace 'white' is not a single point, but a large 'disk', and as such off-whites, are not 'close' to each other. As such when you reduce the brightness, the off-whites contract equally, reducing the variations rather than expanding them. As such the whites just become gray, and not more colorful.

To modulate the image in HSB Color Space you can either use the equivalent technique above, or as of IM v6.5.3-7 you can "-set" the 'special' option 'option:modulate:colorspace' with one of the 'Hue' color spaces. EG: HSL, HSB, or HWB. For example...

  convert wedding_party_sm.jpg \
          -set option:modulate:colorspace HSB -modulate 85 \
          modulate_HSB.png
[IM Output]

Of course if you resized the image to this small size, an even better solution is NOT to save the image to JPEG, which was the cause of the off-white values. Better still don't save the image at all, until after you are finished, so you can keep all the color values at the IM Q16, compile time memory quality setting.

The reason HSB colorspace is not used by default for modulate, is because if you brighten an image in this colorspace the colors become more saturated, and bolder, rather than the image becoming more brilliant and whiter. For example, here is a 150% brighten so you can compare the default HSL with HSB colorspace.

  convert rose:          -modulate 150        mod_bright_HSL.gif
  convert rose: -set option:modulate:colorspace HSB \
                         -modulate 150        mod_bright_HSB.gif
[IM Output]
HSL
[IM Output]
HSB

Before IM v6.4.0-10 the "-modulate" actually did use HSB color space rather than HSL colorspace. This was changed because of a bug report by a user about the above situation.

The point is for some images you are damned if you use HSL, and for other images you are damned if you use HSB colorspace. It just depends on what you are attempting to do!

Solarize Coloring

To "-solarize" an image is to basically 'burn' the brightest colors black. The brighter the color, the darker the solarized color is. This happens in photography when chemical film is over exposed.

  convert rose:   -solarize 90%     solarize.jpg
[IM Output]

Basically anything above the grayscale level given is negated. So if you give a argument of '0%' you basically have a poor man's Negate Operator.

For example here is a faked "-solarize" using a "-fx" mathematical formula.

  convert rose:   -fx  '.9>u ? u : 1-u'     solarize_fx.jpg
[IM Output]

This operator is particularly well suited to extracting the midtone gray colors from images.

For example here I use very strong Sigmoidal Contrast operation to produce a sort of 'fuzzy' threshold at 70% gray. I then Solarize the result to generate a fuzzy-spike rather than a fuzzy-threshold. A final level adjustment then brings the spike to maximum brightness to generate a 'filament' effect.

  convert -size 10x300 gradient: -rotate 90 \
                         -sigmoidal-contrast 50x70%   fuzzy_thres.png
  convert fuzzy_thres.png  -solarize 50%   fuzzy_spike.png
  convert fuzzy_spike.png  -level 0,50%    filament.png
[IM Output] ==> [IM Output] ==> [IM Output]

ASIDE: The above images showing 'profile' graphs of the gradient, was generated using the "im_profile" in the IM Examples, Scripts directory.

Note how anything that is white becomes black, while the mid-tone grays around the central spike are preserved. The fuzziness and placement of the spike is determined by the "-sigmoidal-contrast" operator.

I call it a 'filament' as typically the result looks remarkably like glowing electrical filaments, or lightning discharges. See Random Flux for another example of this effect.

This extraction of mid-tone grays is also put to good use in techniques for generating Edge Outlines from Bitmap Shapes, and for the multiplication of two biased gradients.

Another novel use of this operation is in determining if an image is basically a pure black and white sketch or drawing (such as from a book), rather than a shaded gray-scale or color images, See Determining if an image is: Pure Black and White, or Gray-scale

Recolor Matrix Operator

-recolor   translate, scale, shear, or rotate image colors
           (good for hue changes)

  Each of the rows of the matrix represents the value assignment
  for each channel in the image.
  As such the first three numbers is the color formula for the
  'red' channel. The next for 'green' and so on

  As such the following will swap the red and blue channel color,
  but leave the green channel as is.

        convert rose: -recolor ' 0 0 1
                                 0 1 0
                                 1 0 0 '  rose_blue.png

  And this example makes a grayscale image using a 2/5/3 ratio
  by using the same formula for all color channels.

        convert rose: -recolor ' .2 .5 .3
                                 .2 .5 .3
                                 .2 .5 .3 '  rose_gray_253.png

  The matrix is applied such that each row represents the formula
  for each channel in turn.  As such the first row translates to

       new_red =  .2*red + .5*green + .3*blue

  And so on, with all the colors being calculated before finally being
  assigned back into the original image.


  If a 4x4 matrix is used the transparency channel is also included
  While a 5x5 matrix will also include the 'black' channel for CMYKA images.


  Vivid colors, in a technique called  Digital Velvia
     From http://www.reflectiveimages.com/digitalvelvia.htm
  For example this will produce a 20% vivid color image operation...
        convert rose: -recolor ' 1.2 -0.1 -0.1
                                -0.1  1.2 -0.1
                                -0.1 -0.1  1.2 '  rose_vivid.png

  These matrices brighten that color channel while subtracting
  the colors from the other channels, making colors more vivid
  in the RGB image.  Each row should add to a value of 1.0 to preserve
  the images overall brightness.

  Note that this is not the same as using -modulate to increase an images
  color saturation by 20%.

Recoloring Images with Lookup Tables

While you can recolor images using the various histogram color adjustments as shown above, there is another technique for recoloring images, simply by 'looking up' the modified values from a pre-prepared color gradient, or "Color Look Up Tables" (Color LUT, or CLUT).

Color Lookup Tables

A common requirement of a image processing tool is the ability to replace the whole range of colors, from a pre-prepared table of colors. This allows you to convert images of one set of colors (generally gray-scale) into completely different set of colors, just by looking up its replacement color from a special image.

Of course you do need a 'Look Up Table' image from which to read the replacement colors. For these first few examples, I choose to use a vertical gradient of colors for the LUT so that the IM "gradient:" generator can be used to simplify the generation of the 'color lookup table'.

Well so much for the theory. Let try it out by recoloring a simple gray-Scale Plasma image, replacing the grayscale with a dark-blue to off-white gradient of colors.

  convert -size 100x100 plasma:fractal -virtual-pixel edge -blur 0x5 \
          -shade 140x45  -normalize \
          -size 1x100 xc:black -size 9x100 gradient: \
          +append  gray_image.jpg
  convert -size 10x100  gradient:navy-snow       gradient_ice-sea.png
  convert gray_image.jpg  gradient_ice-sea.png -clut  gray_recolored.jpg
[IM Output] ==> [IM Output] ==> [IM Output]

The "-clut" operator takes two images. The first is the image to replace color values in, the second is a gradient image that is either a single row, or a single column.

The "-clut" operator was added to IM v6.3.5-8.

If your IM is too old to understand the "-clut" operator or you want to do something out of the ordinary, such as a 2 dimensional color lookup table, then you can roll your own using the General DIY Operator, FX. For example here is a slow, but equivalent command to the above.

  convert gray_image.jpg  gradient_ice-sea.png \
          -fx 'v.p{0,u*v.h}'  gray_recolored_fx.jpg
[IM Output]

The problem is that even for a simple process such as the above the "-fx" operator is very slow, and has to be designed specifically for either a row or column LUT. But it does work.

The LUT does not have to be very large. For example here we use a very small LUT, with a very limited number of colors.

  convert -size 1x6 gradient:navy-snow  gradient_levels.png
  convert gray_image.jpg  gradient_levels.png  -clut  gray_levels.jpg
[IM Output] ==> [IM Output] ==> [IM Output]

I enlarged the gradient image for the web page display above, otherwise it would be too small to see properly. The LUT is in actual fact only 6 pixels in size. However if you look at the result you will see that the Color Lookup Operator smoothes out those 6 colors into a smooth gradient.

What is happening is that IM is doing an Interpolated Lookup of the LUT image. That is, instead of just picking the color found, it does a weighted average of all the nearby colors to better represent the LUT. In this particular case, it used the default 'Bilinear' setting that just links each colored pixel together with linear line segments.

Different "-interpolate" settings generate different levels of smoothing of the colors when using a very small color LUT. Here for example I show a various type of interpolated smoothing of the LUT colors.

  convert gray_image.jpg  gradient_levels.png \
          -interpolate Integer         -clut  gray_levels_integer.jpg
  convert gray_image.jpg  gradient_levels.png \
          -interpolate NearestNeighbor -clut  gray_levels_nearest.jpg
  convert gray_image.jpg  gradient_levels.png \
          -interpolate Average         -clut  gray_levels_average.jpg
  convert gray_image.jpg  gradient_levels.png \
          -interpolate BiLinear       -clut  gray_levels_bilinear.jpg
  convert gray_image.jpg  gradient_levels.png \
          -interpolate BiCubic         -clut gray_levels_bicubic.jpg
  convert gray_image.jpg  gradient_levels.png \
          -interpolate Spline          -clut  gray_levels_spline.jpg
[IM Output]
Integer
[IM Output]
Nearest Neighbor
[IM Output]
Average
[IM Output]
BiLinear
[IM Output]
BiCubic
[IM Output]
Spline

The 'Integer' and 'NearestNeighbor' settings are special in that they do no smoothing colors at all. That is, no new 'mixed colors' will be added, only the exact color values present will be used used to color a grayscale image. However note how the lookup of the colors are differ between the two. It is a subtle difference but important.

The 'Average' setting on the other hand also generated bands of color but only using a mix of the colors, resulting in one less color than the size of the color lookup table image.

This type of color 'banding' (or Blocking Artifacts) is actually rather common for geographic maps, and temperature graphs, as it gives a better representation of the exact shape of the map. The sharp boundary edges are known as iso-lines. Adding a slight one pixel Blur to the final image can improve the look of those edges, making it look a little smoother, without destroying the color banding.

The 'BiLinear' setting will also generate banding but only in the form of sharp gradient changes, as will 'BiCubic' to a lesser extent. This is easily seen in the above.

To avoid this problem you would normally use much longer LUT to produce a larger range of intermediate colors. Ideally this should cover the full range of possible intensity values. For ImageMagick Q16 (compiled with 16 bit quality) that requires a LUT to have a height of 65536 pixels. However thanks to Pixel Interpolation, a LUT gradient image of 500 pixels or more is usually good enough for re-coloring most images quite well.

Note that the vertical gradient LUT used in the above examples appears upside-down to our eyes, as the black or '0' index is at the top of the image. Normally we humans prefer to see gradients with the black level at the bottom (thanks to our evolutionary past).

If you rather save the gradient image the 'right way up' you can "-flip" the image as you reading it in. For example lets try a more complex LUT, flipping the vertical gradient before using it on the image.

  convert -size 1x33 gradient:wheat-brown gradient:brown-lawngreen \
          gradient:dodgerblue-navy   -append  gradient_planet.png
  convert gray_image.jpg \
          \( gradient_planet.png -flip \) -clut   gray_planet.jpg
[IM Output] ==> [IM Output] ==> [IM Output]

As you can see for a vertical gradient, flipping it before using makes a lot of sense.

For more examples of generating gradients see Gradients of Color.

You may also be interested in a way of tiling greyscale images using a image for each grey level, which can produce even better 'map' like images. See Dithering with Patterns.

Function to Color LUT Conversion

These pre-prepared "Lookup Table Images" (or LUTs) can also be used to greatly increase the speed of very complex and thus slow "-fx" operations, so instead of IM interpreting the functional string 3 or 4 times per pixel, it can do a much faster lookup of the replacement color.

The procedure for doing this is quite straight forward, either apply the function to a unmodified linear gradient, or replace the 'u' in the function with the value '(i/w)' or '(j/h)' to calculate the replace value based on its position.

For example, in the advanced 'Aqua' Effects example, I used a complex "-fx" function to adjust the gray-scale output of the Shade operator". Also as this gray-scale adjustment is also overlaid onto a 'DodgerBlue' shape, there is no reason why the results of both of these operators could not be combined into a single gradient lookup table.

That is, we generate a LUT from the "-fx" formula and the color overlay. Also for these examples I decided to generate a single row of pixels rather than a column as I did previously.

  convert -size 1x512 gradient: -rotate 90 +matte \
          -fx '3.5u^3 - 5.05u^2 + 2.05u + 0.3' \
          -size 512x1 xc:DodgerBlue -compose Overlay -composite \
          aqua_gradient.png
[IM Output]

The polynomial "-fx" in the above can now be generated more directly and faster using a Polynomial Function. For example
"-function Polynomial 3.5,-5.05,2.05,0.3"

This pre-generated LUT can now be applied to the shaded shape much more quickly at the minimal cost of storing a very small image.

  convert -font Candice -pointsize 72 -background None label:A \
          -trim +repage  aqua_mask.png
  convert aqua_mask.png -alpha Extract -blur 0x6 -shade 120x21 \
          -alpha On -normalize  aqua_shade.png
  convert aqua_shade.png  aqua_gradient.png -clut aqua_font.png
[IM Output] ==> [IM Output] ==> [IM Output]
WARNING: the above is incomplete (edges have not been darkened)
As you can see, the result is very effective, and once an appropriate LUT gradient has been generated, you can re-use the same gradient over and over, as many times as you want.

CLUT and Transparency Handling

The "-clut" operator is controlled by the "-channel" setting, but in reality, it only replaces the individual channel values within the image.

That means that normally each individual channel of the source image is used to 'lookup' the replacement value for just that channel from the color lookup table. That includes the alpha/matte channel which is usually very inconvenient, and difficult to apply.

Typically the "-clut" operator is used to either colorize a gray-scale source image, (see previous examples), OR it is used to do a histogram adjustment of a color image using a gray-scale CLUT (Color Lookup Table). In other words, usually one of the images will typically be gray-scale.

As of IM v6.4.9-8, if a "-channel" setting specifies that if you are wanting to replace/adjust the alpha channel of an image (an 'A' is present), and either the 'source' image or 'CLUT' image has no alpha/matte channel defined, then IM will assume that that image is gray-scale, and will act accordingly.

For example, here I generate a simple blurred triangle, as a grey-scale image. I can then color using a Color Lookup Table that includes transparency. I did not flip the CLUT image this time, so the black replacement will be at the top and white replacement at the bottom.

  convert -size 100x100 xc:  -draw 'polygon 50,10 10,80 90,80' \
          -blur 0x10  blurred_shape.jpg
  convert -size 1x5 xc:none \
          -draw 'fill red    point 0,2' \
          -draw 'fill yellow rectangle 0,0 0,1'   gradient_border.png
  convert blurred_shape.jpg -alpha off    gradient_border.png \
          -channel RGBA  -interpolate integer -clut  clut_shape.png
[IM Output] ==> [IM Output] ==> [IM Output]

Remember the above will only work as expected if the gray-scale image has no alpha channel (turned off using either "-alpha off" or "+matte"), and you specify that you also want to lookup alpha channel values (using "-channel RGBA").

And here is the other special case where were have an image with transparency (and alpha channel) that needs to be adjusted using a gray-scale histogram adjustment gradient (with no alpha channel enabled).


  convert -size 100x100 xc:none -draw 'polygon 50,10 10,80 90,80' \
          tile_disks.jpg -compose In -composite shape_triangle.gif
  convert shape_triangle.gif -channel A -blur 0x10 +channel shape_blurred.png
  convert -size 1x50 gradient: xc:black -append -flip \
          -sigmoidal-contrast 6x0%  feather_histogram.jpg
  convert shape_blurred.png \( feather_histogram.jpg -alpha off \) \
          -channel A    -clut    shape_feathered.png
[IM Output] ==> [IM Output] ==> [IM Output] ==> [IM Output]

The above is a typical Image Feathering problem. The 'black' halo in the intermediate image is caused by the "-blur" operation making the fully-transparent areas surrounding the triangle visible. As fully-transparent has an undefined color, IM defaults to black. The CLUT image itself was designed to ensure that any pixel that was less than 50% transparent will be turned fully-transparent, effectively making the previously fully transparent parts of the image, transparent again.

For this example I over-do the initial 'blur', then over-correct the alpha channel adjustment. The result is a sever rounding of the points of the triangle. For normal image feathering would typically use much smaller values for both the "-blur" and the "-sigmoidal-contrast" alpha adjustment.

Fred Weinhaus, has implemented a blurred feathering technique in his "feather" script, to make it easier to use.

Hald 3D Color Lookup Tables

As of IM v6.5.3-4 you can now also use a full 3D Color Lookup Table which can be used to directly replace all the colors of multiple images. That is, instead of just looking up the value of each each color channel as a separate entity (as in the CLUT above), the whole color is used to lookup the new color.

However a 3D color tables usually require special file formats to correct store the 3D array of color values. However by using a special arrangement of color values the 3D table can be stored into a 2D image known as a Hald Color LUT. This is just a normal image and as such ANY good image file format can be used to save a Hald 3D Color LUT.

To generate a Hald 3D color table, use the 'HALD:{level}' image generator. For example, here is a small one that I have enlarged so you can see the individual pixels...

  convert   hald:3    hald_3.png
[IM Output]

The table holds a color cube with a side of '{level}2' colors or 9 colors. The full color cube contains '9 × 9 × 9' colors, giving a total of 729 colors, which is stored in a image of 27x27 pixels.

The colors are stored so the first 9 colors (in the top-left corner) forms a gradient going from 'pure-black' to 'pure-red'. Every 9th color then forms a gradient in 'green', and every 27th color will form a gradient of 'blue'. The last color in the bottom-right corner is 'pure-white'. You can think of the image as an even simpler 1D array of pixels that are referenced as a 3D color cube, if it helps you to imagine it.

Now this is only a small Hald CLUT image. More typically you would use at least a level 8 Hald (the default), which will hold a color cube with 256 colors per side, or 16777216 colors, and produce an image that is 512x512 pixels in size. Such a "hald:" image has every possible 24 bit color possible (3 channels at 8 bit color depth), and saves into an approximately 10 Kbyte PNG image. Still quite small as images go.

However a smaller Hald image can be used, as IM will interpolate the neighbouring 8 colors from the Hald to work out the final color replacement. But will simply not be as good a representation as a larger version. Hald images' larger than 8 are nor recommended.

Now these generated hald images are the 'identity' or 'no-op' CLUT images. That is, they are the normal colors values forming the 3D color cube, and as such will produce no change the image. For example lets apply a 'no-op' Hald image, using the "-hald-clut" operator...

  convert rose:   hald_3.png -hald-clut   rose_hald_noop.png
[IM Output] ==> [IM Output] ==> [IM Output]

This image is exactly the same as the original, and the Hald image contained no changes.

However by modifying the Hald image, either by hand, or using a color modification, then you can substitute the original colors for the modified colors. For example here I create a blended-sepia-tone color scheme...

  convert hald_3.png \( +clone -sepia-tone 60% \) -average hald_sepia.png
  convert rose.png   hald_sepia.png -hald-clut   rose_hald_sepia.png
[IM Output] ==> [IM Output] ==> [IM Output]

Of course if you can apply a specific color modification to a Hald image, you can also apply it to the actual image directly too. But you can now save your color modifications to reuse them, and can then be applied as many times as like. That means you can spend your effort on the halt, and save it for the future.

You can also send, or download Hald CLUT images for other people and even other applications. You could even directly edit the colors in a Hald, using a image editor like "Gimp" or "Photoshop", or if saved in a Enumerated Pixel Text Image use a plain text editor! All this is especially the case for very complex color modifications

For more details and examples of Hald Images, see Hald Images, Clut Technology.

Please mail me any Hald CLUT images you have found interesting or useful, and I will example them here. You will be credited, here as well!

Hald CLUT Limitations

Unlike the simpler 1 Dimensional gradient lookup using the CLUT Operator you can use a Hald CLUT to rotate colors. For example swap red and blue colors. It is a much more versatile CLUT method. However it is not as good for doing simpler things like coloring a gray-scale image, or doing a histogram adjustment of color values.

It can also replace colors with transparent, or semi-transparent values, by saving such replacement colors in the Hald CLUT image. However this replacement lookup is by color only. You cannot use it to replace transparent colors in specific ways. It isn't after all a 4D color lookup hyper-cube!

Color Replacement using Hald CLUT

Now as the whole color value is used to lookup the color replacement, you could also use this as a method of directly replacing all the colors in an image with some other color.

However as IM currently does a linear interpolated lookup of the Hald, you will need to set the replacement color in all 8 neighbouring color cells of the 3D color cube.

Under Construction

This needs more work, and may need a 'nearest-neighbour' Hald Lookup setting (say using -interpolate), rather than a 3D linear interpolated lookup to work better for specific color replacement. Also some easy way of locating specific colors in a Hald (nearest-neighbour, or the 8 neighbours) would make this a lot easier.

If you have ideas, suggestions, or better still small examples, then please contribute by mailing them to me, or the IM Discussion Forums

Another idea is that if you have two images, the original and the converted, then it should be possible to fill-in a Hald CLUT image from the comparison of the two images. When the immediate colors have been filled in the rest of the color cube should be able to be at least roughly derived by curve fitting the colors that are present. That is, create a 4-D color surface from the color changes discovered.

When complete than you can apply the Hald CLUT to any other image so as to either make the same color transformation (in either direction) to any other image.


Replacing Colors in Images

ImageMagick naturally provides a number of options to replacing a specific and near match colors with another color. This is great when dealing with icons and 'bitmap' type images that contain very few colors, but tends to fail when dealing with images containing shades of colors or anti-aliasing edge pixels.

Basically you need to remember that colors are replaced by a single shade. So if you replace a set or neighbourhood of colors, all those colors are replaced by one specific single color and not by a matching range of colors. That is not to say it is impossible to do a shaded color replacement, just not simple to do at this time, without a lot of work. suggestions welcome.

Even so, GIF images does not allow the use of semi-transparency, so replacing colors in this way is a good method for controlling GIF background transparency (See GIFs on a Background Pattern for examples)

The other aspect is that while you can map all 'close colors' to a given colormap, using Pre-Defined Color Maps, you cannot currently do a global remapping of one set of colors to another completely different set. This is a short coming that may change in a future version of IM.

With that caveat, lets look at the ways IM does provide for the direct replacement of specific colors with another color.

Replace a Specific Color

The "-opaque" and "-transparent" operators are designed for replacing one color in an image with another.

For example to replace a 'blue' color with say 'white' you would use a command like this...

  convert balloon.gif  -fill white -opaque blue   balloon_white.gif
[IM Output] ==> [IM Output]

Basically any color that was 'blue' has been replaced with the current "-fill" color.

However as of IM v6.2.7, this operator is limited by the "-channel" setting. As such, to convert a color (say blue) to transparency, you will need to specify a "-channel" to include the alpha channel to make colors transparent. You will also need to ensure the image has a 'matte' or alpha channel enabled, to hold the transparency information.


  convert balloon.gif   -alpha set  -channel RGBA \
                        -fill none -opaque blue   balloon_none.gif
[IM Output] ==> [IM Output]

Because replacing a color with transparency is such a common operation the above has its own special replace with transparency operator "-transparent".

  convert balloon.gif  -transparent blue   balloon_trans.gif
[IM Output] ==> [IM Output]

As of IM version 6.3.7-10, the 'plus' versions of these operators inverts the color selection. That is, the colors that do NOT match the given color will be replaced. For example here I replace any color that is NOT pure-black, with white, leaving just the pure black borders of the image.

  convert balloon.gif  -fill white +opaque black   balloon_borders.gif
[IM Output] ==> [IM Output]

This may not seem like much, but when you combine it with a Fuzz Factor (see below), this becomes a very powerful tool.

Before IM v6.3.7-10, the inverse operation required the use of some trickiness using image masks. Basically you replace the color you want to preserve with transparency, then "-colorize" all the other colors to the desired color to create an overlay mask. This is then overlaid on the original image to 'mask out' the colors that did not match!

  convert balloon.gif \
          \( +clone -matte -transparent black \
             -fill white  -colorize 100% \) \
          -composite    balloon_mask_non-black.gif

[IM Output] ==> [IM Output]

As you can see the 'plus' form of the operator simplified the 'not this color' replacement operation enormously.

For more advanced replacement techniques, I suggest you look at Transparency Masking and Background Removal.

Be warned that as all matching colors (especially 'fuzzy matched colors', see below) is replaced with a single uniform color, you will not get any anti-aliasing of the edges of the colored areas. And you will lose any an all shadow or other shading effects that may be present. This can have a seriously detrimental effect to the look off any non-simple non-cartoon like images.

This type of color replacement is not designed with practical real world images in mind, but more for image masking effects. Caution is advised.

The "-opaque" color replacement cannot replace a color with a tiled pattern. It will only replace colors with another single specific color. However both the "-draw" and "-floodfill" color replacement methods can (see below).

Replace using a Color in the Image

You can also use Draw Color Replacement to recolor images based on colors present in the image itself, rather than a specific color.

  convert present.gif -fill red -draw 'color 0,0 replace' present_blue.gif
[IM Output] ==> [IM Output]

Note that I never specified the color to be replaced, only the location of the color to be replaced. It is the color at that location that is used for 'matching' what areas is to be filled, regardless of what that color is.

You can see in the above example the problem with color replacement, the specific color may appear in other places that you intend, giving us a line of red pixels within the 'present' image above.

Transparency also presents no problem, though some internal parts of the image was also made transparent just as they became red in the above...

  convert present.gif -matte -fill none \
                      -draw 'color 0,0 replace' present_none.gif
[IM Output] ==> [IM Output]

Note however that unlike "-opaque" and "-transparent" the Draw Color Replacement, does not let you invert the 'matching colors' to be replaced.

Draw also has a special Matte Replacement, where only the transparency of the fill color is replaced. That is, you can make all matching colors transparent, or semi-transparent, without actually changing the color of the pixel itself. With the appropriate file format of course.

  convert present.gif -matte -fill '#00000080' \
            -draw 'matte 0,0 replace' present_semi.png
[IM Output] ==> [IM Output]

This becomes much more useful when a Fuzz Factor is also specified.

The biggest advantage of using "-draw" is that you can also replace the color with a tile pattern. For example..

  convert present.gif -tile pattern:right30 \
                -draw 'color 0,0 replace' present_tile.gif
[IM Output] ==> [IM Output]

For more advanced replacement techniques, I suggest you look at Transparency Masking and Background Removal.

Floodfill Draw

The Draw Color methods also provide you with a simple method of replacing a color by 'floodfilling'. That is, rather than replacing ALL the matching colors within the image, you can select just the colors which are 'connected to' or 'attached' to the specified point in the image.

Again the specified point given sets the starting (or center) of the colors which are to be replaced.

  convert present.gif -fill red -draw 'color 0,0 floodfill' present_fill.gif
[IM Output] ==> [IM Output]

Note that the red areas which was not 'attached' to the 0,0 pixel was note replaced.

For background replacing that can be a problem, but the solution is just as easy. Expand the image slightly so the floodfill can 'leak' into the image from all directions, then remove that extra space when finished.

  convert present.gif -bordercolor white -border 1x1 \
          -fill red     -draw 'color 0,0 floodfill' \
          -shave 1x1               present_bgnd.gif
[IM Output] ==> [IM Output]

Of course you can adjust what colors are 'matched' using the Fuzz Factor control setting below, which is especially important for JPEG images.

Floodfill Operator

The "-floodfill" operator was added to make floodfilling slightly easier, especially when you what to exactly specify the 'center' color for the Fuzz Factor color matching.

That is, you not only specify the start or seed point for the flood fill, but also the center color, of the colors you are replacing.

  convert present.gif -bordercolor white -border 1x1 \
          -fill red    -floodfill +0+0 white \
          -shave 1x1               present_floodfill.gif
[IM Output] ==> [IM Output]

This will replace any color that is 'white' to 'red' that is directly part of the area surrounding the seed pixel starting at +0+0.

Note that the 'seeding pixel' must itself be a match to the 'white' color otherwise no action will be taken. This 'do nothing if no match' is particularly useful to ensure that the color of the area specified is the expected color, such as when doing a scripted color replacement.

If you are attempting to replace a background color, adding a border of the same color, to allow the "-floodfill" to 'leak' all the way around the edges of the image, is a good idea. This was done in the previous example.

You can also floodfill with a tile pattern.

  convert present.gif -bordercolor white -border 1x1 \
                -tile pattern:left30   -floodfill +0+0 white \
                -shave 1x1           present_pattern.gif
[IM Output] ==> [IM Output]

Of course if the colors are not exact, as it is in the above I recommend that you also specify a "-fuzz" factor, so that nearby colors also 'match' the given color. See next set of examples.

For more advanced replacement techniques, I suggest you look at Transparency Masking and Background Removal.

Fuzz Factor - Matching Similar/Multiple Colors

The overall results of just selecting a single color to replace, as shown in the previous examples is usually not very nice. The edges or areas of solid colors generally have a mix of colors at the edge, due to anti-aliasing (See Anti-Aliasing for more information). As such you should avoid direct color replace if possible.

For example here I take what looks like a simple black and white 'cow' and try to make it a red cow.

  convert cow.gif -fill red -opaque black  cow_replace_red.gif
[IM Output] ==> [IM Output]

As you can see only the center parts of the 'black' areas actually became red. That is because, while the image appears to be black and white it is really a gray-scale image with almost all the edges various shades of gray. That is, they are not exactly pure-black in color.

The fuzz factor, ("-fuzz") represents a 'similarity' match in multi-dimensional spherical distance between colors, using whatever color space the image is using.

Well okay lets try that in plain English. You have a specific color. Another color will be treated as being same as the color being looked for, if the difference between these colors is less than the fuzz factor setting. The larger the 'fuzz factor' and more 'near' colors will match and be replaced.

So lets try that on our cow image so as to convert not only pure-black but also near-black colors to red.

  convert cow.gif -fuzz 40%  -fill red -opaque black  cow_replace_fuzz.gif
[IM Output] ==> [IM Output]

As you can see we now replaced all the 'dark' pixels of the image to red. But the result is still very bad, with a grayish tinge to the edge, and strong Aliasing effects. Direct color replacement is not a good solution for this image, even though you can make it work using a large 'fuzz factor' . See Level adjustments by Color for an alternative.

This problem is even worse for images where you are trying to replace a background color with transparency. You basically end up with a 'halo' around the object on that background color. This is very difficult to solve, and problems like this are looked at in detail in Re-adding Transparency to an Image.

The "-fuzz" setting is a color 'distance' setting. Any color that is within the given distance of the color being looked for, will match that color, even though it is not an exact match.

A value of '200' represents a distance of 200 color units in the current color depth of the IM being used. For a IM Q16 (16 bit quality for color store) this is quite small, for a IM Q8 this is VERY large, and will cause a lot of colors to match each other.

Here for example I change all the colors that are within 3000 color units (for IM Q16) of 'blue' to white. With my Q16 ImageMagick programs, that represents about the distance from 'blue to 'navy blue' (about 25% as a percentage, see below).

  convert colorwheel.png -fuzz 30000 -fill white -opaque blue opaque_blue.jpg
[IM Output]

To make this easier to understand here I invert the matched colors turning the unmatched colors to white.

  convert colorwheel.png \
          -fuzz 30000 -fill white +opaque blue \
          opaque_blue_not.png
[IM Output]

If your IM is older than version 6.3.7-10 when the 'plus' form of the "-opaque" operator was added, you can use this masking method to invert the result of the color match...

  convert colorwheel.png \
          \( +clone  -fuzz 30000 -transparent blue \
             -fill white  -colorize 100% \) \
          -composite   opaque_blue_inv.png
[IM Output]

Or this method that limits all modifications to just the 'alpha channel', so that all the original colors, are left as is. That is, you create a negated mask from the color selection, so as to make all non-selected colors fully-transparent. They remain present, just transparent!

  convert colorwheel.png \
          -channel A -fuzz 30000 -transparent blue -negate +channel \
          opaque_blue_inv_alpha.png
[IM Output]

An advantage of these methods is that you can expand them to generate a 'not multiple colors' technique. All that you need to do is add more colors to the list being made transparent, before negating the mask.

  convert colorwheel.png -channel A \
          -fuzz 25000 -transparent blue -transparent red -transparent lime \
          -fuzz 11000 -transparent black   -negate   +channel \
          -background white -flatten   opaque_multi_inv.png
[IM Output]


As a matter of interest, in a IM with a Q8 compilation setting, a "-fuzz" factor of 256 (28) will make the colors 'black' and 'blue' the same. For a IM with a Q16 setting this number is 65536 (216).

To make 'blue' and 'red' colors match this number must be multiplied by the square root of 2, or 362 for IM Q8, and with 92682 for IM Q16.

Finally to make all colors match (eg colors 'black' and 'white') you will need to multiply by the square root of 3. In other words, a fuzz factor setting of 444 for IM Q8 and 113512 for IM Q16.

As you can see from the above formulas, direct color distances is definitely not a nice way of setting the fuzz factor to use, as it is also dependant on exactly what compile time Quality Setting is used.

Setting the "-fuzz" factor as a percentage, makes its use a lot simpler. In this case '100%' represents a large enough fuzz factor to cover all colors. That is, it represents the color distance from 'black' to 'white', across the 3 dimensional diagonal of the RGB color cube.

To demonstrate lets change 90% of all the colors closest to 'white', white. This should result in only the last 10% colors near 'black' on the image, as black is on the opposite side of the RGB color cube.

  convert colorwheel.png -fuzz 90% -fill white -opaque white  opaque_w90.jpg
[IM Output]

Note that as the 90% represents a sphere of colors around 'white' in RGB color space. This however is not the same as replacing the colors that are not within a 10% sphere of black.

  convert colorwheel.png -fuzz 10% -fill white +opaque black  opaque_k10.jpg
[IM Output]

As you can see 10% sphere of colors near the black is much more uniform, than selecting a 90% sphere of the colors around white.

A "-fuzz" factor of 100%, equates to the RGB color cube distance from 'black' to 'white'. From this we can calculate that a percentage of about 57.7% is the distance between 'black' and 'blue', and 81.6% is the distance from 'blue' to 'red' or from either of those colors to 'white'.

In summary, anything larger than about 25%, (just short of the RGB distance from 'blue' to 'navy blue' represents a very large color change.

To demonstrate the color distances more, lets use a progressively larger fuzz factor percentage around the blue colors...

  convert colorwheel.png -fuzz 10% -fill white -opaque blue opaque_b10.jpg
  convert colorwheel.png -fuzz 25% -fill white -opaque blue opaque_b25.jpg
  convert colorwheel.png -fuzz 57% -fill white -opaque blue opaque_b57.jpg
  convert colorwheel.png -fuzz 81% -fill white -opaque blue opaque_b81.jpg
  convert colorwheel.png -fuzz 95% -fill white -opaque blue opaque_b95.jpg
[IM Output] [IM Output] [IM Output] [IM Output] [IM Output]

From this you can clearly see that it isn't 'black', or 'white' that is the most distant color from 'blue', but that it is actually 'yellow' that is most distant within RGB color space.

Also note that a 81% color difference will just miss matching a pure 'red' color, however while pure red does not match almost all of the other reds do. That is again due the to 'spherical' nature of the color matching. The moral is that you are probably better of either using multiple small "-fuzz" factored matches or a smaller 'inverted match', than a single large value.

Color matching is actually much more 'natural' if the image was stored using some other color scheme than RGB, such as YIQ. The formula is still the same, just using a different colorspace. Similarly for other color spaces.

How to use YIQ or even CMYK with opaque tests ????
Colors do not seem to map correctly in matching to images in a different
colorspace. 
Using a "-fuzz" factor becomes more complicated when matching involves transparent and semi-transparent colors. As of IM version 6.2.6-2 the comparison algorithm was updated so that fully-transparent colors will always match as being the same color, no matter what the actual RGB color components are.

Comparing semi-transparent colors will results in the distance between the RGB color components being divided by amount of transparency involved, as such two semi-transparent colors are closer together than their fully-opaque equivalents.

This improves comparisons between images containing transparencies, and also when color reducing images with some semi-transparency, with less semi-transparent colors being generated in color reductions.

The "-fuzz" operator effects just about any operator which compares specific colors within an image. This includes: "-opaque", "-transparent", "-floodfill", "-trim", "-deconstruct", "-draw 'color'", "-draw 'matte'", and probably others. It also effects GIF "-layers OptimizeTransparency", and "-compose ChangeMask" handling.

Full Color Map Replacement

FUTURE: Replace all the colors in one color map to another. Suggestions as to how to best do this is welcome, or programmers to implement some image color map function. One method may be to use ideas presented in Dithering with Symbols.

The best known solution (but hardly ideal) is currently provided by Fred Weinhaus in is "mapcolors" script. This script essentially maps each color one at a time, masking the pixels involved from one image into a new initially blank image.


Under Construction

More color options yet to be looked at in detail...

Color Cycling?
    -cycle     shift colormap (for animations of fractals???)

Color maths (get the average of two or more colors)....

  Example Averaging two colors... Say '#000000'  and  '#DDDDDD'

  Generally the colors are added to images, and the result output as a
  single pixel 'txt:-' image, which which the color can be extracted.

  * use -resize to merge the colors

      convert -size 2x1 xc:'#000000' -fill '#DDDDDD' \
              -draw 'point 0,0'  -resize 1x1  txt:-

  * Use -average on them!

      convert -size 1x1 xc:'#000000' xc:'#DDDDDD' \
              -average  txt:-

    Or for a lot of colors you can use the 'Box' resize filter
      convert rose: -filter Box -resize 1x1\! txt:
      # ImageMagick pixel enumeration: 1,1,255,RGB
      0,0: (145, 89, 80) #915950

  * Use -fx to apply whatever formula you want

      convert -size 1x1 xc:'#000000' xc:'#DDDDDD' \
              -fx '(u+v)/2'  txt:-

  With a ImageMagick API the results can be more directly retrieved from the
  image.

Chromaticity Color Points???
   –white-point x,y
   –red-primary x,y
   –green-primary x,y
   –blue-primary x,y


Created: 19 December 2003
Updated: 19 July 2009
Author: Anthony Thyssen, <A.Thyssen@griffith.edu.au>
Examples Generated with: [version image]
URL: http://www.imagemagick.org/Usage/color/