## Writing a Composite ImageCommand – Gamma Corrected Resampling

We have a customer who expressed some concern over how our resample command works (and how many similar commands work, in general).  He pointed us to an article here which describes the problem.  The short of it is that the energy in a pixel is an expression of power (Watts) over the area of the pixel and although the scale of the pixel component intensities is represented as linear, it is really exponential.  Averaging a set of power measurements linearly produces results which don’t effectively represent the average power.  The trick is compensate for this is to make the power scale linear by applying an inverse gamma function to the image, then resampling it and finally applying the original gamma function to restore it.

The gamma function looks like this:

pv’ = pvγ

where pv is the normalized value for the channel of a pixel – this means it is a value between 0 and 1 inclusive.  γ is a value > 0, typically 2.2 for most monitors.  So the typical process of converting an 8-bit per channel pixel is something like this:

foreach pixel
foreach channel
double normalizedChannel = channel / 255.0;
double channelPrime = Math.Exp(normalizedChannel, gamma);
channel = Math.Floor(channelPrime * 255);


DotImage has an ImageCommand for performing this transformation efficiently.  We can do the transformation I’ve described as a sequence of steps, but I thought it would be nice to combine everything into one command.

The first thing to do is to make a subclass of ImageCommand.  ImageCommand is an abstract class which is, more or less the a collection of GoF template methods.  This is to make it easy to implement ImageCommands from scratch.  We’re not doing that – we’re making a composite command.   We’ll implement the abstract methods, but they won’t be needed.  So let’s start off with an outline of what we want:

public class GammaResampleCommand : ImageCommand
{
GammaCommand _gammaCommand = new GammaCommand();
ResampleCommand _resampleCommand = new ResampleCommand();
public GammaResampleCommand()
{
}
public GammaResampleCommand(double gammaLevel, Size destSize, ResampleMethod method)
{
GammaLevel = gammaLevel;
DestSize = destSize;
Method = method;
}
protected override AtalaImage PerformActualCommand(AtalaImage source, AtalaImage dest, System.Drawing.Rectangle imageArea, ref ImageResults results)
{
throw new NotImplementedException();
}
public override PixelFormat[] SupportedPixelFormats
{
get { return _gammaCommand.SupportedPixelFormats; }
}
protected override void VerifyProperties(AtalaImage image)
{
ImageCommand.ProxyVerifyProperties(_gammaCommand, image);
ImageCommand.ProxyVerifyProperties(_resampleCommand, image);
}
public double GammaLevel
{
get { return _gammaCommand.GammaLevel; }
set { _gammaCommand.GammaLevel = value; }
}
public Size DestSize
{
get { return _resampleCommand.DestSize; }
set { _resampleCommand.DestSize = value; }
}
public ResampleMethod Method
{
get { return _resampleCommand.Method; }
set { _resampleCommand.Method = value; }
}
}


You’ll note that rather than implement the properties or VerifyProperties, we’re just acting as a proxy to existing instances of GammaCommand and ResampleCommand.  This means we don’t need to implement error checking on the properties – that will happen for free.  In fact, to implement VerifyProperties, we’re taking advantage of a feature of ImageCommand – ImageCommand has a set of methods named ProxyXXXX, which will call the method XXXX for us, even though it is protected.  This breaks encapsulation, but it is there just for this purpose – creating composite commands.  Finally, you’ll note that PerformActualCommand throws an exception.  That’s OK – we’re going to ensure that it is never called.

What remains is to do the actual work, and for that we’ll override the method Apply().

    public override ImageResults Apply(AtalaImage image)
{
using (AtalaImage gammaCorrected = image.Clone() as AtalaImage)
{
double gamma = _gammaCommand.GammaLevel;
double inverseGamma = 1 / gamma;
GammaLevel = inverseGamma;
_gammaCommand.Apply(gammaCorrected);
AtalaImage finalImage = _resampleCommand.Apply(gammaCorrected).Image;
GammaLevel = gamma;
_gammaCommand.Apply(finalImage);
return new ImageResults(finalImage, false);
}
}


GammaCommand is InPlaceProcessing = true, which means that it side-effects the source image.  I don’t want this, so I’ll make a copy of the source image using Clone, then apply the inverse gamma to the source, resample it, then apply the normal gamma.

Here is some quick test code for this:

using (AtalaImage image = new AtalaImage(@"..\..\gamma_dalai_lama_gray.jpg"))
{
GammaResampleCommand resampler = new GammaResampleCommand(2.2,
new Size(image.Width / 2, image.Height / 2),
ResampleMethod.LanczosFilter);
using (AtalaImage smaller = resampler.Apply(image).Image)
{
smaller.Save("dalailama.jpg", new JpegEncoder(), null);
}
}


The input file is this:

and the subsequent output is here:

Published Wednesday, March 03, 2010 11:05 AM by Steve Hawley