位图和像素位

来源于MSDN


代码下载位置:  Foundations2008_06.exe  (159 KB) 
在线浏览代码

Windows ® Presentation Foundation (WPF) 的保留模式图形系统给 Windows 图形编程带来了巨大变化。程序不再需要在系統要求时重新在屏幕上创建自己的可视外观。这个复合系统会保留所有图形数字,并将其安排到整个可视外观中。
保留模式图形确实能够简化工作流程,但是对于 Windows 编程人员来说,“简便”本身并非是首要考虑目标。实际上,正是保留模式图形系统与通知机制(依赖关系属性)的组合,使 WPF 的灵活性和功能得以展现。图形对象(如路径和画笔)看起来仍“存在”于复合系统中,并继续响应属性更改和图形转换,因此允许对这些对象进行数据绑定和设置动画效果。
最近,我发现 WPF 位图具有类似的动态性质:呈现的位图仍会响应更改 — 不仅能够响应图形转换(众所周知),还能响应位图中实际像素位的更改。
展现这种动态响应的两个位图类是 RenderTargetBitmap 和 WriteableBitmap,它们是由 BitmapSource 派生的 9 个类中的成员;BitmapSource 是一个抽象类,是 WPF 中的所有位图支持的基础。程序可以将其中某个位图对象与图像元素一起显示、使用 ImageBrush 类将其制成一支平铺画笔,或者使用 ImageDrawing 类将其用作大型绘图(可能混有矢量图形)的一部分,但无论以哪种方式使用,都不会在呈现位图后就将其忽略。相反,位图仍然位于可视复合系统中,并继续响应应用程序更改。
虚拟实验室:位图、像素和 WPF
WPF 位图通过图形转换和更改位图中的像素来响应更改。在此虚拟实验室中,您可以使用 RenderTargetBitmap 和 WriteableBitmap 类使位图响应应用程序中的更改。所需环境已全部设置完毕,Charles Petzold 的示例代码也已准备就绪,因此,当您阅读本期“基础”专栏时可立即开始编码。

在虚拟实验室中进行试验:
使用 RenderTargetBitmap
RenderTargetBitmap 是一个位图,通过将 Visual 类型的对象传输到其表面即可进行有效绘制。要创建 RenderTargetBitmap 类型的新对象,唯一的方法就是使用构造函数,并需要向该构造函数提供位图的像素尺寸、以每英寸点数表示的水平和垂直分辨率,以及 PixelFormat 类型的对象。
稍后,我将详细介绍 PixelFormat 结构和相关的静态 PixelFormats 类。要创建 RenderTargetBitmap 类型的对象,您必须将 PixelFormats.Default 或 PixelFormats.Pbgra32 用作 RenderTargetBitmap 构造函数的最后一个参数。无论使用哪一个,都可以创建一个 32 位/像素的透明位图。
最初,RenderTargetBitmap 对象是完全透明的。然后,您可以使用 Visual 类型的对象(包括从 Visual 派生的类,如 FrameworkElement 和 Control)来调用 Render 方法,从而在此位图上进行绘制。通过调用 Clear,可还原完全透明的图像。如果当前显示的是该位图,则上述调用将立即反映在显示的位图中。
图 1 显示了一个可演示 RenderTargetBitmap 的完整的小程序。该程序将创建一个宽 1,200 像素、高 900 像素的位图,它们对应位图对象的 PixelWidth 和 PixelHeight 属性。每像素的宽度都是 4 个字节,因此该位图占用的内存将超过 4 兆字节。
class RenderTargetBitmapDemo : Window
{
    RenderTargetBitmap bitmap;

    [STAThread]
    public static void Main()
    {
        Application app = new Application();
        app.Run(new RenderTargetBitmapDemo());
    }
    public RenderTargetBitmapDemo()
    {
        Title = "RenderTargetBitmap Demo";
        SizeToContent = SizeToContent.WidthAndHeight;
        ResizeMode = ResizeMode.CanMinimize;

        // Create RenderTargetBitmap
        bitmap = new RenderTargetBitmap(1200, 900, 300, 300, 
                                        PixelFormats.Default);

        // Create Image for bitmap
        Image img = new Image();
        img.Stretch = Stretch.None;
        img.Source = bitmap;
        Content = img;
    }
    protected override void  OnMouseDown(MouseButtonEventArgs args)
    {
        Point ptMouse = args.GetPosition(this);
        Random rand = new Random();
        Brush brush = new SolidColorBrush(
            Color.FromRgb((byte)rand.Next(256), (byte)rand.Next(256), 
                                                (byte)rand.Next(256)));

        DrawingVisual vis = new DrawingVisual();
        DrawingContext dc = vis.RenderOpen();
        dc.DrawEllipse(brush, null, ptMouse, 12, 12);
        dc.Close();

        bitmap.Render(vis);
    }
    protected override void OnClosed(EventArgs args)
    {
        PngBitmapEncoder enc = new PngBitmapEncoder();
        enc.Frames.Add(BitmapFrame.Create(bitmap));
        FileStream stream = new FileStream("RenderTargetBitmapDemo.png", 
                                      FileMode.Create, FileAccess.Write);
        enc.Save(stream);
        stream.Close();
    }
}
调用 RenderTargetBitmap 构造函数还会指定 300 点/英寸的分辨率。将像素尺寸和分辨率相结合可以创建一个宽 4 英寸、高 3 英寸的位图。在与设备无关的 WPF 坐标系中,一英寸为 96 个单位,因此该位图的设备无关宽度是 384 个单位,设备无关高度是 288 个单位。如果查看由 BitmapSource 定义的 Width 和 Height 属性,就会看到这些数字。
RenderTargetBitmapDemo 程序使用图像元素显示未进行拉伸的位图。它还会捕获 MouseDown 事件。每次单击鼠标,该程序都会创建一个 DrawingVisual 对象 — 一个直径为 ¼ 英寸的实心小圆,然后调用 Render 方法将此圆添加到位图图像中。然后此圆就会出现在显示的位图上。您可以将此程序看作一个简单的画图应用程序,此程序在单个位图中融合了呈现和存储功能。
您可能知道(或者可能会猜到),图像元素通过调用在执行其 OnRender 方法的过程中传递给它的 DrawingContext 对象的 DrawImage 方法来显示位图。当 RenderTargetBitmapDemo 程序更改位图时,Image 类不会重复调用其 OnRender 方法。在可视复合系统中,对位图所做的这些更改发生在更深层。
RenderTargetBitmapDemo 程序终止时,它会以 PNG 文件格式保存合成的位图。如果查看由 RenderTargetBitmapDemo 保存的位图,您会发现其大小是 1,200 × 900 像素,并且每个圆的直径都是 75 像素 — 如果以 300 点/英寸为单位,则为 ¼ 英寸。
使用 WriteableBitmap
WriteableBitmap 类包含两个构造函数,其中一个与 RenderTargetBitmap 构造函数非常相似。前四个参数是位图的像素尺寸和分辨率(以每英寸点数为单位)。第五个参数是一个 PixelFormat 对象,但它比 RenderTargetBitmap 的灵活度要高。对于需要调色板的位图格式,WriteableBitmap 构造函数还提供了另外一个用于调色板的参数。
WriteableBitmap 的像素都将初始化为零,其具体意义取决于像素格式。在许多情况下,位图是全黑的。如果位图支持透明度,则位图就是透明的。如果位图有调色板,则整个位图将显示为调色板中的第一种颜色。
更改 WriteableBitmap 与更改 RenderTargetBitmap 有着很大的区别。对于 WriteableBitmap,您必须调用名为 WritePixels 的方法,该方法将本地数组中的实际像素位复制到位图中。很显然,数组中数据的格式和大小与位图的尺寸和像素格式相匹配至关重要。
我们先看一个相对简单的示例。 图 2 中显示的 AnimatedBitmapBrush.cs 程序将创建一个 WriteableBitmap,并将其用作平铺 ImageBrush 的基础图块,其中的平铺 ImageBrush 已由该程序设为窗口的 Background 属性。然后,此程序会将计时器设为 100 毫秒,重复调用 WritePixels 来更改位图。
class AnimatedBitmapBrush : Window
{
    const int COLS = 48;
    const int ROWS = 48;

    WriteableBitmap bitmap;
    byte[] pixels = new byte[COLS * ROWS];
    byte pixelLevel = 0x00;

    [STAThread]
    public static void Main()
    {
        Application app = new Application();
        app.Run(new AnimatedBitmapBrush());
    }
    public AnimatedBitmapBrush()
    {
        Title = "Animated Bitmap Brush";
        Width = Height = 300;

        bitmap = new WriteableBitmap(COLS, ROWS, 96, 96, 
                                     PixelFormats.Gray8, null);
        ImageBrush brush = new ImageBrush(bitmap);
        brush.TileMode = TileMode.Tile;
        brush.Viewport = new Rect(0, 0, COLS, ROWS);
        brush.ViewportUnits = BrushMappingMode.Absolute;
        Background = brush;

        DispatcherTimer tmr = new DispatcherTimer();
        tmr.Interval = TimeSpan.FromMilliseconds(100);
        tmr.Tick += TimerOnTick;
        tmr.Start();
    }

    void TimerOnTick(object sender, EventArgs args)
    {
        for (int row = 0; row < ROWS; row++)
            for (int col = 0; col < COLS; col++)
            {
                int index = row * COLS + col;

                double distanceFromCenter = 
                    2 * Math.Max(Math.Abs(row - ROWS / 2.0) / ROWS,
                                 Math.Abs(col - COLS / 2.0) / COLS);
                pixels[index] =
                    (byte)(0x80 * (1 + distanceFromCenter * pixelLevel));
            }

        bitmap.WritePixels(new Int32Rect(0, 0, COLS, ROWS), pixels, COLS, 0);
        pixelLevel++;
    }
}
COLS 和 ROWS 值都是常量,用于定义此位图的像素尺寸。这两个值还适用于平铺画笔 Viewport 矩形,并且还用在计时器的 Tick 事件处理程序中。像素格式的设置为 PixelFormats.Gray8,这意味着位图中的每个像素都由指示灰影的 8 位值表示 — 像素值 0x00 代表黑色,0xFF 代表白色。由于 Gray8 格式不需要调色板,因此 WriteableBitmap 构造函数的最后一个参数设为空。
由于每个像素占 1 个字节,所以我称之为“像素”的字节数组尺寸按 COLS × ROWS 计算即可。该像素数组将被定义为字段,因此无需在每次调用 Tick 事件处理程序时都重新创建该数组。该数组中的数据必须从最顶行开始自左向右排列,然后是第二行,依此类推。Tick 事件处理程序中包含两个循环,分别针对位图的行和列,但是它将这两个值组合到了一个一维数组索引中:
   int index = row * COLS + col;
WritePixels 不会接受多维数组。WritePixels 的第一个参数是一个以像素坐标为单位的 Int32Rect 结构,用于指示位图中要更新的矩形子集。Int32Rect 对象的 X 和 Y 属性指示矩形左上角相对于位图左上角的坐标;Width 和 Height 属性指示此矩形的像素尺寸。要更新整个位图,可将 X 和 Y 设置为 0,并将 Width 和 Height 设置为位图的 PixelWidth 和 PixelHeight 属性。稍后,我将介绍一下 WritePixels 的最后两个参数。
必须承认,我原计划为此位图编码的动画模式与此模式有些然不同,但此处偶然发现的模式看起来相当有趣 — 至少有小部分如此。 图 3 中显示了其中一个图像。
图 3  AnimatedBitmapBrush 显示
像素数组
如果位图不是 1 个字节/像素的格式,且只更新了位图的矩形子集,则调用 WritePixels 的过程可能会更加复杂。这是一个需要了解的重要技术,因为静态 BitmapSource.Create 方法需要使用这种相同的数组格式创建新位图,BitmapSource 的 CopyPixels 方法也需要使用此数组格式将位图中的像素位复制到数组中。在所有这三个方法中,您都可以选择使用 IntPtr 指向本地缓冲区,但下面我将重点介绍数组方法。
位图中的像素总数可以根据 BitmapSource 类中定义的 PixelWidth 和 PixelHeight 属性得出。BitmapSource 还会定义 PixelFormat 类型的只读 Format 属性,该属性自身还会定义一个名为 BitsPerPixel 的只读属性,属性值范围是 1 到 128。从一种极端的角度来说,一个字节可以存储连续 8 像素的数据;而从另一种极端的角度来说,每个像素需要 16 个字节的数据。您可能仅对像素位使用 byte、ushort、uint 或 float 的数组。
您提供给 WritePixels 方法的 Int32Rect 对象可在位图中定义一个矩形子集。像素数组中的字节数必须包含足够的数据,以填充 Int32Rect 对象指示的行数和列数。这有些复杂,因为多个像素格式将在一个字节中存储多个像素。对于这些格式,每行数据都必须从字节边界处开始。
例如,假设您正在使用的位图采用 4 位/像素的格式。位图中您正在访问或更新的矩形区域宽 5 像素,高 12 像素。这两个值是您提供的 Int32Rect 对象的 Width 和 Height 属性值。数组中的第一个字节包含前两个像素的数据,第二个字节包含接下来两个像素的数据,但是第三个字节只包含第一行中第五个像素的数据。下一个字节对应于第二行的前两个像素。
每行数据都需要 3 个字节,整个矩形区域需要 36 个字节。为了帮助完成此计算,WritePixels 方法需要使用一个名为 stride 的参数,这是每行像素数据的字节数。Stride 的常规计算方法为:
int stride = (width * bitsPerPixel + 7) / 8;
宽度等于 Int32Rect 结构的 Width 属性值。即使使用 ushort、uint 或 float 值数组,stride 值也始终以字节为单位。然后,您就可以使用以下方法计算数组中的总字节数:
  int dimension = height * stride;
如果使用的是 ushort、uint 或 float 数组,请分别除以 2、4 或 8。
您也许还记得 Windows API 要求每行位图数据都要从 32 位内存边界开始,因此 stride 值必须是四的倍数。但在 WPF 中则无需如此。不过,如果方便的话,您可以将 stride 值设置为大于通过公式计算出的值。例如,您可能使用每像素 1 个字节的位图,但是您的数组类型是 uint,而不是 byte。在这种情况下,数组中的每个元素都将存储四个像素。即使并未严格要求,您可能仍希望在单位边界处开始数组的每一行,因为在目前的许多硬件平台上,对齐的副本通常比未对齐的副本执行速度快。
位图像素格式
位图中的每个像素都由定义该位图颜色的一个或多个位表示。在 WPF 中,特定的像素格式由结构类型为 PixelFormat 的对象表示。静态 PixelFormats 类可定义 PixelFormat 类型的 26 个静态属性,供您在创建位图时使用。这些属性在 图 4 中分两组显示:可写格式和不可写格式。除了 Bgr555、Bgr565 和 Bgr101010 这三个例外,属性名称中的任何数字都与每像素的位数相同。
可写格式
Indexed1
Indexed2
Indexed4
Indexed8
 
BlackWhite
Gray2
Gray4
Gray8
 
Bgr555
Bgr565
 
Bgr32
Bgra32
Pbgra32
不可写格式
默认
 
Bgr24
Rgb24
Bgr101010
Cmyk32
 
Gray16
Rgb48
Rgba64
Prgba64
 
Gray32Float
Rgb128Float
Rgba128Float
Prgba128Float
使用 BitmapSource.Create 创建位图时,您可以使用 PixelFormats 类的任一静态属性,但 PixelFormats.Default 除外。创建 WriteableBitmap 类型的位图时,只能使用可写格式。
以单词 Indexed 开头的格式要求在静态 BitmapSource.Create 方法或 WriteableBitmap 构造函数中使用 ColorPalette 对象。每个像素都是 ColorPalette 对象的一个索引,因此这四个格式分别最多可与 2、4、16 和 256 种颜色相关联。如果实际的像素位未达到上限,则 ColorPalette 无需与最大颜色数相同。
对于低于 8 位/像素的格式,字节中最明显的位对应于最左侧的像素。例如,对于 Indexed2 格式,字节 0xC9 等效于二进制 11001001,并对应于四个 2 位值(11、00、10 和 01)。那么,这四个值就分别对应于 ColorPalette 集合中的第 4 个、第 1 个、第 3 个和第 2 个颜色。
BlackWhite、Gray2、Gray4 和 Gray8 格式是采用 1、2、4 或 8 位每像素的灰影位图。像素全部为 0 代表黑色,而像素全部为 1 代表白色。
另外的五个可写格式是颜色格式。字母 B、G 和 R 分别代表蓝色、绿色和红色。字母 A 代表 Alpha 通道,并指示位图支持透明度。字母 P 代表预乘 Alpha,稍后我会对此进行介绍。
Bgr555 和 Bgr565 格式都要求采用 16 位(或 2 个字节)/像素。Bgr555 格式对每个原色使用 5 个位(因而存在 32 个层次),余下 1 位。如果蓝原色的位使用 B0(最不明显的位)到 B4(最明显的位)表示,绿色和红色也采用类似的表示法,则两个连续的数据字节就可以存储这三种原色,如 图 5 所示。
图 5  双字节像素格式(单击图像可查看大图)
请注意,绿色位将遍布这 2 个字节。如果您了解到像素实际上是一个 16 位的无符号整数,最先存储的字节最不明显,那么这种安排将更有意义。 图 6 中的关系图显示了如何使用一个短整数来对原色编码。
图 6  短整数像素格式(单击图像可查看大图)
如果一来您就可以确保: 图 7 中显示的 Gradient555Demo 程序将创建此格式的位图,并在其中写入像素,显示从左到右、从蓝到绿的渐变。请注意,ushort 数组的大小是总行数和列数的乘积。
class Indexed2Demo : Window
{
    const int COLS = 50;
    const int ROWS = 20;

    [STAThread]
    public static void Main()
    {
        Application app = new Application();
        app.Run(new Indexed2Demo());
    }
    public Indexed2Demo()
    {
        Title = "Bgr555 Bitmap Demo";

        WriteableBitmap bitmap = new WriteableBitmap(COLS, ROWS, 96, 96,
                                        PixelFormats.Bgr555, null);

        ushort[] pixels = new ushort[ROWS * COLS];

        for (int row = 0; row < ROWS; row++)
            for (int col = 0; col < COLS; col++)
            {
                int index = row * COLS + col;
                int blue = (COLS - col) * 0x1F / COLS;
                int green = col * 0x1F / COLS;
                ushort pixel = (ushort)(green << 5 | blue);
                pixels[index] = pixel;
            }

        int stride = (COLS * bitmap.Format.BitsPerPixel + 7) / 8;
        bitmap.WritePixels(new Int32Rect(0, 0, COLS, ROWS), pixels,
                           stride, 0);

        Image img = new Image();
        img.Source = bitmap;
        Content = img;
    }
}
此代码演示了使用不同于对应于每像素字节数的 byte 类型的其他数组类型的好处。对于任何行和列的像素地址,数组索引只是使用行与每列像素数相乘,再使用乘积与列取和所得的结果。
Bgr565 格式与 Bgr555 非常相似,但在前者中绿色使用了 6 位,眼睛对绿色最敏感。另外三个可写格式使用起来要容易得多。从蓝色开始,所有格式都采用每像素 4 个字节。在 Bgr32 格式中,最后 4 个字节为零;不透明。在另外两个格式中,第四个字节是 Alpha 通道。Alpha 值的范围是 0x00(透明)到 0xFF(不透明)。将像素视为 32 位无符号整数时,最不明显的 8 位用于对蓝色编码,最明显的 8 位为 0 或 Alpha 通道。
通常,位图中字节的顺序对应于属性名中字母 B、G、R 和 A 的顺序。不可写格式的列表最先列出的是几个仅使用 16 或 24 位/像素的颜色格式。Bgr101010 格式使用 32 位/像素,但每个原色使用 10 位/像素。使用 32 位无符号整数表示像素时,最不明显的 10 位用于蓝色。Cmyk32 格式对打印时使用的蓝绿色,紫红色,黄色和黑色级别进行编码。
Gray16、Rgb48、Rgba64 和 Prgba64 格式对 16 位灰影和 16 位原色进行编码。如果您使用的硬件和应用程序要求更高的颜色精度(如医疗图像),那么您可能很高兴现在能够存储和显示分辨率如此高的位图数据。然而,其他情况下就不必非得使用这些格式。在 8 位/原色显示器上,或保存为 8 位/原色文件格式时,会忽略额外的颜色精度。
像素格式列表提供了四种使用单精度浮点值表示颜色级别和透明度的格式。这些格式基于伽玛值为 1 的 scRGB 颜色空间,而不是惯用的伽玛值为 2.2 的 sRGB 颜色空间。(请参阅我编写的《Applications = Code + Markup》一书中的第 24-25 页查看相关说明。)浮点颜色值 0.0 对应于字节值 0x00(黑色),浮点颜色值 1.0 对应于字节值 0xFF,但对于显示设备,浮点颜色值可能会大于 1,因为其颜色域宽度大于视频显示器。
PixelFormats 勘误表
PixelFormats 文档中的部分格式似乎有些混乱。Gray16、Rgb48、Rgba64 和 Prgba64 格式都是基于伽玛值 1 记录的,但是除 Gray16 外,其余格式又以 sRGB 格式进行了记录,这就产生了矛盾。事实上这并不会引起混乱,因为只有 Float 像素格式使用 scRGB 颜色格式和伽玛值 1。
您可能希望使用一种通用方法将像素拆分到其颜色组件中,或者基于颜色组件构建像素。PixelFormat 结构提供了一个名为 Mask 的属性,这是 PixelFormatChannelMask 类型对象的集合。按蓝色、绿色、红色和 alpha 顺序,每个颜色通道都有一个对应的 PixelFormatChannelMask 对象。
PixelFormatChannelMask 结构定义了一个 Mask 属性,这是一个字节集合,字节数等于每像素字节数,还对应于像素的字节顺序。例如,对于 Bgr555 格式,有三个 PixelFormatChannelMask 对象,每个对象包含 2 个字节。对于蓝色,这两个字节是 0x1F 和 0x00;对于绿色,是 0xE0 和 0x03;对于红色,是 0x00 和 0x7C。若要使用此数据,您必须派生自己的移位因子。
我说过,您可以对 BitmapSource.Create 方法使用除 PixelFormats.Default 以外的任何 PixelFormats 成员,但对 WriteableBitmap 构造函数只能使用 图 4 中的可写格式。如果查看 WriteableBitmap 文档,您将发现一个备用构造函数,可使用任何 BitmapSource 对象创建 WriteableBitmap 对象。
实际上,您可以先使用 图 4 中的不可写格式创建一个 BitmapSource 对象,然后再基于此 BitmapSource 创建 WriteableBitmap。但其中存在以下限制:任何采用不可写格式的位图都将转换为 Bgr32 或 Pbgra32 格式,具体取决于 alpha 通道的状态。
您可以将创建的所有位图保存到任何支持格式的文件中,这些文件格式包括 BMP、GIF、PNG、JPEG、TIFF 和 Microsoft Windows Media Photo。不过,在此过程中可能会将位图数据转换为其他格式。例如,另存为 GIF 文件时,总是先将位图转换为 Indexed8 格式;另存为 JPEG 文件时,总是将位图转换为 Gray8 或 Bgr32。到目前为止,PixelFormat 和 BitmapEncoder 的任何组合都不能为您生成包含超过 8 位/原色的数据的文件。
预乘 Alpha
PixelFormats 类的三个静态属性均以字母 P 开头,代表预乘 Alpha。此技术用于改进部分透明的像素的位图呈现效率,仅适用于拥有 Alpha 通道的位图。
假设您已创建了一个使用以下方式计算颜色的 SolidColorBrush:
Color.FromArgb(128, 0, 0, 255)
那是一支半透明的蓝色画笔。显示该画笔时,颜色必须与显示器表面的现有颜色结合。如果在黑色背景上进行绘制,则合成的 RGB 颜色为 (0,0,128)。如果在白色背景上进行绘制,合成的颜色将为 (127,127,255)。这是一种简单的加权平均计算。
下列公式的下标指示在现有表面上呈现部分透明像素的结果:
Rresult = [(255 – Apixel) * Rsurface + Apixel * Rpixel] / 255;
Gresult = [(255 – Apixel) * Gsurface + Apixel * Gpixel] / 255;
Bresult = [(255 – Apixel) * Bsurface + Apixel * Bpixel] / 255;
如果像素的 R、G 和 B 值已经乘以 A 值并除以 255,则可以提高此计算的速度。这样即可取消每个公式中的第二个乘法运算。例如,假设 Bgra32 位图中的像素是 ARGB 值 (192, 40, 60, 255)。在 Pbgra32 位图中,同一像素则为 (192, 30, 45, 192)。RGB 值已经乘以 Alpha 值 192 并除以 255。
对于 Pbgra32 位图中的任何像素,R、G 或 B 值都不应大于 A 值。这样就不会出现任何错误。虽然这些值最大可为 255,但是您无法得到所需的透明度级别。
WriteableBitmap 应用程序
您可能会根据需要在应用程序中利用 WriteableBitmap 来显示一些简单的动态图形(比如条形图),您会发现与使用 WPF 绘制相应的矢量图形相比,更新位图的速度更快。
也许 WriteableBitmap 最常见的用途是执行实时图像处理和非线性转换。TwistedBitmap 项目允许您加载任何 8 位/像素或 32 位/像素的位图,并使用 Slider 控件使该图像在其中心周围发生扭曲,如 图 8 所示。图像越小,效果越好。
图 8  扭曲的位图(单击图像可查看大图)
该程序使用 BitmapFrame.Create 加载来自文件的位图,然后调用 CopyPixels 复制 pixelsSrc 数组中的所有像素位。 图 9 中显示的 SliderOnValueChanged 事件处理程序负责将像素从 pixelsSrc 转换到用于调用 WritePixels 的 pixelsNew 数组中。
void SliderOnValueChanged(object sender,
                          RoutedPropertyChangedEventArgs<double> args)
{
    if (pixelsSrc == null)
        return;

    Slider slider = sender as Slider;
    int width = bitmap.PixelWidth;
    int height = bitmap.PixelHeight;
    int xCenter = width / 2;
    int yCenter = height / 2;
    int bytesPerPixel = bitmap.Format.BitsPerPixel / 8;

    for (int row = 0; row < bitmap.PixelHeight; row += 1)
    {
        for (int col = 0; col < bitmap.PixelWidth; col += 1)
        {
            // Calculate length of point to center and angle
            int xDelta = col - xCenter;
            int yDelta = row - yCenter;
            double distanceToCenter = Math.Sqrt(xDelta * xDelta +
                                                yDelta * yDelta);
            double angleClockwise = Math.Atan2(yDelta, xDelta);

            // Calculate angle of rotation for twisting effect 
            double xEllipse = xCenter * Math.Cos(angleClockwise);
            double yEllipse = yCenter * Math.Sin(angleClockwise);
            double radius = Math.Sqrt(xEllipse * xEllipse +
                                      yEllipse * yEllipse);
            double fraction = Math.Max(0, 1 - distanceToCenter / radius);
            double twist = fraction * Math.PI * slider.Value / 180;

            // Calculate the source pixel for each destination pixel
            int colSrc = (int) (xCenter + (col - xCenter) *
                                Math.Cos(twist)
                (row - yCenter) * Math.Sin(twist));
            int rowSrc = (int) (yCenter + (col - xCenter) *
                                Math.Sin(twist) 
                + (row - yCenter) * Math.Cos(twist));
            colSrc = Math.Max(0, Math.Min(width - 1, colSrc));
            rowSrc = Math.Max(0, Math.Min(height - 1, rowSrc));

            // Calculate the indices
            int index = stride * row + bytesPerPixel * col;
            int indexSrc = stride * rowSrc + bytesPerPixel * colSrc;

            // Transfer the pixels
            for (int i = 0; i < bytesPerPixel; i++)
                pixelsNew[index + i] = pixelsSrc[indexSrc + i];
        }
    }
    // Write out the array
    bitmap.WritePixels(rect, pixelsNew, stride, 0);
}
使用位图转换时,执行反向转换至关重要。如果检查原始位图中的每个像素并确定它在新位图的位置,很可能会出现原始位图中的不同像素映射到了新位图中的同一像素的情况。这就意味着,将不会为新位图中的某些像素设置任何值!该图像将出现一些“孔”。
相反,对于新位图中的每个像素,您必须了解原始位图中的哪些像素映射到特定的行或列。此方法可确保新位图中的每个像素都具有经计算得出的值。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值