在 WPF 的后台线程中使用像素着色器






4.92/5 (8投票s)
如何在后台进程中应用像素着色器。
引言
本文介绍了如何在 WPF 组件中使用带有着色器效果的组件,同时使其对用户不可见。这使得可以在后台批量处理图像着色。
有许多关于着色器以及如何将其用作 WPF 组件效果的优秀示例,本文将不关注像素着色器的这一方面。有关其中一些文章,请参阅“准备工作”下的链接。
在“关注点”中,您将找到有关后台渲染问题的答案。
背景
我有很多文件夹,里面是这样的黑白图像:
并希望将它们转换为更容易看的样式。例如
我没有任何图形程序能够一次性对多张图片应用批量效果。快速搜索批量图像效果或类似内容并没有真正给出任何解决方案。据报道,Photoshop 具有宏/批处理功能,但并非所有人都有(并且理解)Photoshop,对吧?我没有……
因此,搜索扩展到程序化解决方案。搜索结果形成了您现在看到的项目和文章。
使用代码
准备
1. 如果尚未安装,请安装 DirectX SDK 。
旁注:这是一个 2010 年的 API,但最近有一些活动,并且有将像素着色器集成到 Visual Studio 中的趋势。更多信息请参阅: http://blogs.msdn.com/b/chuckw/archive/2012/05/07/hlsl-fxc-and-d3dcompile.aspx
在不久的将来,我们大概就可以从代码编译像素着色器,而不再需要 fxc 了。
在此之前,我们需要调用它,所以
2. 将 fxcPath 项目设置更改为 fxc.exe 的完整路径。
有关 fxc 的更多信息(官方): http://msdn.microsoft.com/en-us/library/windows/desktop/bb509710.aspx 。其他文章也提供了关于 fxc 和着色器的良好解释和应用:
- https://codeproject.org.cn/Articles/26425/WPF-Bitmap-Effects-using-Pixel-Shaders-Net3-5sp1-B
- https://codeproject.org.cn/Articles/226547/Mandelbrot-Set-with-PixelShader-in-WPF
- https://codeproject.org.cn/Articles/36722/Create-Reflection-Shader-in-Silverlight
演示
您现在可以运行演示项目,查看各种像素着色器脚本对您选择的图像的效果。选择一个文件夹和一个文件过滤器(例如 *.png),然后单击其中一个“着色”按钮。它会将您在着色器文本框中提供的着色器脚本应用于匹配您过滤器的图像文件,并将结果另存为新的文件,文件名为 shaded.filename.png
示例输出
WingsLikeEagles.png:
shaded.WingsLikeEagles.png
在您自己的项目中用法
没有单独的 dll 项目,对于您自己的项目,您只需要 BackgroundImageShader.cs 和 CustomShaderEffect.cs,您可以根据自己的喜好进行更改。
这是一个为单个文件着色的示例代码片段。
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 会调用一些其他函数。最好浏览一下这些函数,看看发生了什么。调用只深入了几层,要了解基本概念应该不是什么大问题。
关注点
在后台渲染
与许多其他着色器示例一样,我们使用一个 WPF Image 组件,并将着色器设置为 Effect。有一些额外的步骤需要完成,因为我们使用的 Image 不在可见窗口中,并且引擎足够智能,不会费力渲染一个孤立的图像控件。以下是这些步骤:
1. Image img
嵌入在 Viewbox 中(同样是离屏的)
(img
和 viewbox
是 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
和 viewbox
被设置为正确的比例,还对 viewbox 调用了一些布局函数。这使得控件能够以应用了着色器的状态进行渲染。
/// <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. 要获取图像的内容,将执行一次“屏幕截图”:
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);
}
在这里,正确设置 RenderTargetBitmap 的宽度、高度和 DPI,以及 img
和 viewbox
的宽度和高度非常重要。否则,您可能会丢失图像的一部分,或者降低图像质量,或者以与原始图像不同的尺寸保存它。
请注意,您可以在加载图像时指定不同的宽度/高度(2)。DpiX 和 DpiY 也是 BackgroundImageShader 的属性。大多数时候,它们可以从提供的 BitmapImage 中读取,但并非总是如此。然后,手动将它们设置为源图像的正确值。
在单独的线程中渲染。
为了真正将渲染与 GUI 分开,它还必须在不同的线程中运行。如果您想在单独的线程中使用 WPF 控件,该线程需要 STAThread 属性。我所知道的应用此属性的唯一方法是创建一个传统的 System.Threading.Thread
,如下所示。
这个小类保存了线程参数:着色脚本、图像文件夹、图像过滤器。
class ShaderThreadState
{
public string pixelShader;
public string path;
public string filter;
}
线程:(类似于示例,添加了遍历目录的循环)
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)
));
}
};
以及如何启动它:
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
});
如果您不添加 STA 属性,但仍尝试在线程中运行 BackgroundImageShader,它将会崩溃。现在您知道当它发生时该怎么做了。
下一步是什么?
查看演示按钮背后调用的代码。有一些方便的子函数可供您使用,不一定以与演示相同的方式使用。
其他要做的事情:
- 支持其他图像类型/输出流类型
- 添加异常处理 - 目前没有
- 管理运行的线程
- 如果可能,请将线程机制更改为更优雅/现代的方式
- 如果您发现任何需要澄清或改进的地方,请随时评论!
鸣谢
感谢那位不为人知的作者,他让我对像素着色器产生了兴趣,并且是 PrepareShader() 中大部分代码的作者。我很抱歉我再也找不到并链接到您的文章了。
我使用的文件夹浏览器组件来自此处: http://wpffolderbrowser.codeplex.com/
历史
2013-08-25 发布初始版本