GPU-based image effects (brightness, contrast) for WPF

The Windows Presentation Foundation (WPF) based on the Direct3D rendering pipeline. This allow you to create custom HSLS shader for the image effects. The .NET framework has only two built-in effects:

  • DropShadowEffect
  • BlurEffect

Unfortunately, the more common effects brightness and contrast are not available.

sampler2D Input : register(S0);
float brightness : register(c0);
float contrast : register(c1);

float4 main(float2 uv : TEXCOORD) : COLOR
{
	float4 color = tex2D(Input, uv);

	color.rgb /= color.a;
	color.rgb += brightness;
	color.rgb = ((color.rgb - 0.5) * pow((contrast + 1.0), 2)) + 0.5;
	color.rgb *= color.a;

	return color;
}

The RGB components of the color are divided by the alpha component. This is done to "unpremultiply" the color, which is a common operation when working with colors that have an alpha component. This operation essentially separates the RGB values from the alpha value, allowing them to be adjusted independently.

After the contrast and brightness adjustments are applied, the RGB values are then multiplied by the alpha component again to "premultiply" the color. This is necessary because the final color value is expected to be in premultiplied format, which is the standard format for colors with an alpha component.

Use the Direct3D Shader Compiler (fxc) to compile the pixel shader. (The fxc command is part of the Windows SDK.)

fxc.exe shader.fx /T ps_2_0 /Fo shader.fxc

Include the compiled shader as "Resource".

<ItemGroup>
    <Resource Include="shader.fxc" />
</ItemGroup>

ShaderEffect is a class in WPF that enables developers to apply custom pixel shaders to elements in a WPF application. A pixel shader is a program that runs on the graphics card and performs per-pixel operations on images, such as color adjustments, blurs, and other effects.

public class BrightnessContrastEffect : ShaderEffect
{
       public static readonly DependencyProperty InputProperty =
        ShaderEffect.RegisterPixelShaderSamplerProperty(
            nameof(Input), 
            typeof(BrightnessContrastEffect), 0);

        public static readonly DependencyProperty BrightnessProperty =
            DependencyProperty.Register(
                nameof(Brightness), 
                typeof(double), 
                typeof(BrightnessContrastEffect), 
                new UIPropertyMetadata(0.0, PixelShaderConstantCallback(0)));

        public static readonly DependencyProperty ContrastProperty =
            DependencyProperty.Register(
                nameof(Contrast), 
                typeof(double), 
                typeof(BrightnessContrastEffect), 
                new UIPropertyMetadata(0.0, PixelShaderConstantCallback(1)));

        public BrightnessContrastEffect()
        {
            PixelShader = new PixelShader() 
            { 
                UriSource = new Uri(@"pack://application:,,,/WPF.ImageEffects;component/shader.fxc") 
            };
            UpdateShaderValue(InputProperty);
            UpdateShaderValue(BrightnessProperty);
            UpdateShaderValue(ContrastProperty);
        }

       //....
}

Add the CLR namespace to the root node.

<Window x:Class="WPF.ImageEffects.Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:imageeffects="clr-namespace:WPF.ImageEffects;assembly=WPF.ImageEffects"
        xmlns:local="clr-namespace:WPF.ImageEffects.Demo"
        Title="MainWindow">

Place the shader effect to the image control.

<Slider x:Name="sliderBrightness" Minimum="-1" Maximum="1" Value="0" />
<Slider x:Name="sliderContrast" Minimum="-1" Maximum="1" Value="0" />

<Image x:Name="image">
            <Image.Effect>
                <imageeffects:BrightnessContrastEffect 
                                        Brightness="{Binding Value, ElementName=sliderBrightness}" 
                                        Contrast="{Binding Value, ElementName=sliderContrast}" />
            </Image.Effect>
</Image>