Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Pixel shaders in a background thread in WPF

0.00/5 (No votes)
25 Aug 2013 1  
How to apply pixel shaders in a background process

Introduction

This article shows how to use a WPF component with a shader effect while keeping it invisible to the user. This enables bulk image processing with shaders in the background.  

There are many other good examples of shaders and how to use them as an effect on WPF components, and this article will not focus on that side of pixel shaders. See the links listed under "Preparation" for some of these articles. 

In "Points of interest" you will find answers to the issues of background rendering. 

Background    

I had a ton of folders with black-on-white images looking like this: 

  

and wanted to convert them to something easier on the eyes. For example:

 

None of my graphics programs had the ability to apply an effect in bulk to many pictures at once. A quick search on bulk image effect or the like didn't really present any solution. Photoshop is reported to have macro/batch capability, but not everyone has (and understands) Photoshop, right? I don't..

So the search expanded to programmatic solutions. The results of that formed the project and article you see now.  

Using the code

Preparation 

1. install the DirectX SDK if you haven't already.

side note: This is an API from 2010 but recently there has been some activity and a movement towards integration of pixel shaders into visual studio. More on that here: http://blogs.msdn.com/b/chuckw/archive/2012/05/07/hlsl-fxc-and-d3dcompile.aspx
Somewhere in the near future we can probably compile pixel shaders from code and not need fxc anymore.  

Until then, we need to call it, so:

2. change the fxcPath project setting to the full path to fxc.exe. 

More info on fxc here (official) http://msdn.microsoft.com/en-us/library/windows/desktop/bb509710.aspx . These other articles also offer good explanations and applications of fxc and shaders:  

Demo  

You can now run the demo project to check out the effects of various pixel shader scripts on images you choose. Pick a folder and a file filter like *.png and click one of the Shade buttons. It will apply the shader script you provide in the shader textbox to the image files that match your filter and save the results as new files as shaded.filename.png 

Example output 

WingsLikeEagles.png:  

 

shaded.WingsLikeEagles.png 

Usage in your own project  

There's no separate dll project, for your own projects you only need BackgroundImageShader.cs and CustomShaderEffect.cs which you can change to your liking.  

Here is an example piece of code to shade one particular file. 

string pngPath = "C:\SomePicture.png"
 
BackgroundImageShader bis = new ImageShader();
bis.PixelShaderScript = "Insert shader script here";
 
//Create and initialize a BitmapImage
BitmapImage src = new BitmapImage();
src.BeginInit();
src.UriSource = new Uri(pngPath, UriKind.Relative);
src.CacheOption = BitmapCacheOption.OnLoad;
src.EndInit();
 
//Call the shader
bis.ShadeBitmap(src, string.Format(@"{0}\shaded.{1}",
 System.IO.Path.GetDirectoryName(pngPath, System.IO.Path.GetFileName(pngPath)); 

ShadeBitmap calls some other functions inside. It is probably a good idea to browse through these to see what happens. The calls go only a few levels deep, shouldn't be much of a problem to get the basic idea. 

Points of Interest 

Rendering in the background  

Just like many other shading examples out there, we use a WPF Image component with the shader set as Effect. There are some extra hoops to jump through because the Image we use is not in a visible window and the engine is smart enough not to bother rendering an orphaned image control. Here are the hoops:  

1. the Image img is embedded in a Viewbox (also off-screen)  

(img and viewbox are private variables of BackgroundImageShader)   

//prepare images
img = new Image();
img.Stretch = Stretch.None;
viewbox = new Viewbox();
viewbox.Stretch = Stretch.None;
viewbox.Child = img; //control to render 

2. img and viewbox are sized to the correct proportions, also some layout functions are called on viewbox. this make the controls render with the shader applied.  

/// <summary>
/// Loads the image and viewbox for off-screen rendering.
/// </summary>
public void LoadImage(double width, double height)
{
	img.BeginInit();
	img.Width = width;
	img.Height = height;
	img.Source = Source;
	img.EndInit();
 
	viewbox.Measure(new Size(img.Width, img.Height));
	viewbox.Arrange(new Rect(0, 0, img.Width, img.Height));
	viewbox.UpdateLayout();
} 

3. And to get the contents of the image, a "screenshot" if you will:  

void SaveUsingEncoder(BitmapEncoder encoder, Stream stream)
{
	RenderTargetBitmap bitmap = new RenderTargetBitmap((int)(img.Width * DpiX / WPF_DPI_X), (int)(img.Height * DpiY / WPF_DPI_Y), DpiX, DpiY, PixelFormats.Pbgra32);
	bitmap.Render(viewbox);
	
	BitmapFrame frame = BitmapFrame.Create(bitmap);
	encoder.Frames.Add(frame);
	encoder.Save(stream);
} 

It is important here to get the width,height and dpi of the RenderTargetBitmap right, as well as width and height of img and viewbox. Otherwise you will lose portions of the image or lose quality or save it with different dimensions than the original.   

Note that you can specify a different width/height when loading the image (2). Also DpiX and DpiY are properties of BackgroundImageShader. Most of the time they can be read from a provided BitmapImage, but not always. Then set them to the correct values for your source image manually. 

Rendering in a separate thread. 

To really separate the rendering from the GUI it also has to run in a different thread. If you want to use WPF controls in a separate thread that thread requires the STAThread attribute. The only way that I know of to apply that attribute is to create a good old-fashioned System.Threading.Thread as shown below.

This little class holds the thread parameters: shading script, image folder, image filter. 

class ShaderThreadState
{
	public string pixelShader;
	public string path;
	public string filter;
}  

The thread: (similar to the example, added loop through a directory) 

ParameterizedThreadStart ShaderThreadMulti = (object state) =>
{
    var sts = (ShaderThreadState)state;
 
    //Create imageshader
    BackgroundImageShader bis = new BackgroundImageShader();
    bis.PixelShaderScript = sts.pixelShader;
 
    //Loop through the directory specified in sts.path and shade them one after the other in this thread
    foreach (string pngf in Directory.GetFiles(sts.path, sts.filter, SearchOption.TopDirectoryOnly))
    {
        //Shade every file that filter
        BitmapImage src = new BitmapImage();
        src.BeginInit();
        src.UriSource = new Uri(pngf, UriKind.Relative);
        src.CacheOption = BitmapCacheOption.OnLoad;
        src.EndInit();
 
        bis.ShadeBitmap(src, string.Format(@"{0}\shaded.{1}",
            sts.path,
            System.IO.Path.GetFileName(pngf)
            ));
    }
}; 

And how to start it: 

Thread thread = new Thread(ShaderThreadMulti);
//this is required for the components used in ImageShader.
thread.SetApartmentState(ApartmentState.STA);
thread.Start(new ShaderThreadState()
{
	pixelShader = shaderscript.Text,
	path = sourcefolder.Text,
	filter = filter.Text
}); 

If you don't add the STA attribute and still try to run the BackgroundImageShader it in a thread, it'll crash. Now you know what to do about it when it does. 

What's next? 

Check out what code is called behind the demo buttons. There are a few handy subfunctions that you can use, not necessarily in the same way as the demo.   

Other things to do:  

  • Support other image types / output stream types  
  • Add exception handling - it's nonexistent  
  • Manage running threads   
  • Change thread mechanism to something more elegant/modern if possible 
  • Don't hesitate to comment if you find somthing that needs clarification or improvement!  

Credits 

Thanks to the unknown author of the article that got me interested in Pixel Shaders and is responsible for most of the code in PrepareShader(). I'm sorry that I could not find and link to your article anymore. 

The folder browser component I used came from here: http://wpffolderbrowser.codeplex.com/ 

History 

2013-08-25 Published initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here