Faster algorithm to change Hue/Saturation/Lightness in a bitmap

Trevor Elliott picture Trevor Elliott · Jan 16, 2013 · Viewed 10.9k times · Source

I am trying to filter a Bitmap image to increase or decrease Hue, Saturation, and Lightness values.

My code is working perfectly, but it is slow.

I am locking two bitmaps in memory, the original source and the current destination. The user can move various trackbar controls to modify each value which is then converted to an HSL value. For example, the values on the trackbar correspond to a range of -1.0 to 1.0.

Each time an event is thrown that the trackbar value changed, I run a function which locks the destination bitmap and applies the HSL values with the source bitmap and then stores the result in the destination bitmap. Once finished, I unlock the destination bitmap and paint the image on the screen.

Previously I used a lookup table for my other filters since I was doing per-byte operations. However I do not know how to apply that using HSL instead. Here is the code I am using:

byte red, green, blue;

for (int i = 0; i < sourceBytes.Length; i += 3)
{
    blue = sourceBytes[i];
    green = sourceBytes[i + 1];
    red = sourceBytes[i + 2];

    Color newColor = Color.FromArgb(red, green, blue);

    if (ModifyHue)
        newColor = HSL.ModifyHue(newColor, Hue);

    if (ModifySaturation)
        newColor = HSL.ModifySaturation(newColor, Saturation);

    if (ModifyLightness)
        newColor = HSL.ModifyBrightness(newColor, Lightness);

    destBytes[i] = newColor.B;
    destBytes[i + 1] = newColor.G;
    destBytes[i + 2] = newColor.R;
}

And here's my ModifyBrightness function:

public static Color ModifyBrightness(Color color, double brightness)
{
    HSL hsl = FromRGB(color);
    hsl.L *= brightness;
    return hsl.ToRGB();
}

So basically if their brightness slider is in the very middle, its value will be 0 which I will convert to "1.0" when I pass it in to the function, so it multiplies the brightness by 1.0 which means it won't change. If they drag the slider all the way to the right it will have a value of 100 which will result in a modifier of 2.0, so I'll multiply the lightness value by 2.0 to double it.

Answer

Trevor Elliott picture Trevor Elliott · Jan 17, 2013

I ended up researching ImageAttributes and ColorMatrix and found the performance was excellent.

Here is how I implemented it for a Saturation and Brightness filter:

// Luminance vector for linear RGB
const float rwgt = 0.3086f;
const float gwgt = 0.6094f;
const float bwgt = 0.0820f;

private ImageAttributes imageAttributes = new ImageAttributes();
private ColorMatrix colorMatrix = new ColorMatrix();
private float saturation = 1.0f;
private float brightness = 1.0f;

protected override void OnPaint(object sender, PaintEventArgs e)
{
    base.OnPaint(sender, e);

    e.Graphics.DrawImage(_bitmap, BitmapRect, BitmapRect.X, BitmapRect.Y, BitmapRect.Width, BitmapRect.Height, GraphicsUnit.Pixel, imageAttributes);
}

private void saturationTrackBar_ValueChanged(object sender, EventArgs e)
{
    saturation = 1f - (saturationTrackBar.Value / 100f);

    float baseSat = 1.0f - saturation;

    colorMatrix[0, 0] = baseSat * rwgt + saturation;
    colorMatrix[0, 1] = baseSat * rwgt;
    colorMatrix[0, 2] = baseSat * rwgt;
    colorMatrix[1, 0] = baseSat * gwgt;
    colorMatrix[1, 1] = baseSat * gwgt + saturation;
    colorMatrix[1, 2] = baseSat * gwgt;
    colorMatrix[2, 0] = baseSat * bwgt;
    colorMatrix[2, 1] = baseSat * bwgt;
    colorMatrix[2, 2] = baseSat * bwgt + saturation;

    imageAttributes.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

    Invalidate();
}

private void brightnessTrackBar_ValueChanged(object sender, EventArgs e)
{
    brightness = 1f + (brightnessTrackBar.Value / 100f);

    float adjustedBrightness = brightness - 1f;

    colorMatrix[4, 0] = adjustedBrightness;
    colorMatrix[4, 1] = adjustedBrightness;
    colorMatrix[4, 2] = adjustedBrightness;

    imageAttributes.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

    Invalidate();
}