【问题标题】:Edit raw pixel data of WriteableBitmap?编辑 WriteableBitmap 的原始像素数据?
【发布时间】:2026-02-13 12:25:03
【问题描述】:

是否可以直接读取/写入 WriteableBitmap 的像素数据?我目前正在使用 WriteableBitmapEx 的SetPixel(),但它很慢,我想直接访问像素而没有任何开销。

我有一段时间没有使用 HTML5 的画布了,但如果我没记错的话,您可以将其图像数据作为单个数字数组获取,而这正是我正在寻找的东西

提前致谢

【问题讨论】:

    标签: c# wpf pixel writeablebitmap


    【解决方案1】:

    要回答您的问题,您可以使用Lock、write、Unlock 模式更直接地访问可写位图的数据,如下所示,但通常没有必要,除非您将绘图基于内容的图像。更典型的是,您可以只创建一个新缓冲区并使其成为位图,而不是相反。

    话虽如此,WPF 中有许多扩展点可以在不借助像素操作的情况下执行创新绘图。对于大多数控件,现有的WPF primitives(边框、线条、矩形、图像等)绰绰有余——不要担心使用其中的许多控件,它们使用起来相当便宜。对于复杂的控件,您可以使用DrawingContext 来绘制 D3D 图元。对于图像效果,您可以使用Effect class implement GPU assisted shaders 或使用内置效果(模糊和阴影)。

    但是,如果您的情况需要直接访问像素,请选择 pixel format 并开始写作。我建议使用 BGRA32,因为它易于理解并且可能是最常见的讨论。

    BGRA32 表示像素数据以 4 个字节的形式存储在内存中,依次代表图像的蓝色、绿色、红色和 Alpha 通道。这很方便,因为每个像素都以 4 字节边界结束,将其以 32 位整数形式存储。处理 32 位整数时,请记住在大多数平台上顺序会颠倒(如果需要支持多个平台,请检查 BitConverter.IsLittleEndian 以确定运行时正确的字节顺序,x86 和 x86_64 都是小端序)

    图像数据存储在宽度为stride 的水平条中,构成图像宽度的单行。 stride 宽度始终大于或等于图像的像素宽度乘以所选格式中每个像素的字节数。某些情况会导致步幅比特定于某些架构的width * bytesPerPixel 更长,因此您必须使用步幅宽度来计算行的开始,而不是乘以宽度。由于我们使用的是 4 字节宽的像素格式,因此我们的步幅恰好是 width * 4,但您不应该依赖它。

    如前所述,我建议使用 WritableBitmap 的唯一情况是,如果您正在访问现有图像,下面是示例:

    之前/之后:

    // must be compiled with /UNSAFE
    // get an image to draw on and convert it to our chosen format
    BitmapSource srcImage = JpegBitmapDecoder.Create(File.Open("img13.jpg", FileMode.Open),
        BitmapCreateOptions.None, BitmapCacheOption.OnLoad).Frames[0];
    
    if (srcImage.Format != PixelFormats.Bgra32)
        srcImage = new FormatConvertedBitmap(srcImage, PixelFormats.Bgra32, null, 0);
    
    // get a writable bitmap of that image
    var wbitmap = new WriteableBitmap(srcImage);
    
    int width = wbitmap.PixelWidth;
    int height = wbitmap.PixelHeight;
    int stride = wbitmap.BackBufferStride;
    int bytesPerPixel = (wbitmap.Format.BitsPerPixel + 7) / 8;
    
    wbitmap.Lock();
    byte* pImgData = (byte*)wbitmap.BackBuffer;
    
    // set alpha to transparent for any pixel with red < 0x88 and invert others
    int cRowStart = 0;
    int cColStart = 0;
    for (int row = 0; row < height; row++)
    {
        cColStart = cRowStart;
        for (int col = 0; col < width; col++)
        {
            byte* bPixel = pImgData + cColStart;
            UInt32* iPixel = (UInt32*)bPixel;
    
            if (bPixel[2 /* bgRa */] < 0x44)
            {
                // set to 50% transparent
                bPixel[3 /* bgrA */] = 0x7f;
            }
            else
            {
                // invert but maintain alpha
                *iPixel = *iPixel ^ 0x00ffffff;
            }
    
            cColStart += bytesPerPixel;
        }
        cRowStart += stride;
    }
    wbitmap.Unlock();
    // if you are going across threads, you will need to additionally freeze the source
    wbitmap.Freeze();
    

    但是,如果您不修改现有图像,则确实没有必要。例如,您可以使用所有安全代码绘制棋盘图案:

    输出:

    // draw rectangles
    int width = 640, height = 480, bytesperpixel = 4;
    int stride = width * bytesperpixel;
    byte[] imgdata = new byte[width * height * bytesperpixel];
    
    int rectDim = 40;
    UInt32 darkcolorPixel = 0xffaaaaaa;
    UInt32 lightColorPixel = 0xffeeeeee;
    UInt32[] intPixelData = new UInt32[width * height];
    for (int row = 0; row < height; row++)
    {
        for (int col = 0; col < width; col++)
        {
            intPixelData[row * width + col] = ((col / rectDim) % 2) != ((row / rectDim) % 2) ?
                lightColorPixel : darkcolorPixel;
        }
    }
    Buffer.BlockCopy(intPixelData, 0, imgdata, 0, imgdata.Length);
    
    // compose the BitmapImage
    var bsCheckerboard = BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgra32, null, imgdata, stride);
    

    如果你直接写入字节数组,你甚至不需要一个 Int32 中间体。

    输出:

    // draw using byte array
    int width = 640, height = 480, bytesperpixel = 4;
    int stride = width * bytesperpixel;
    byte[] imgdata = new byte[width * height * bytesperpixel];
    
    // draw a gradient from red to green from top to bottom (R00 -> ff; Gff -> 00)
    // draw a gradient of alpha from left to right
    // Blue constant at 00
    for (int row = 0; row < height; row++)
    {
        for (int col = 0; col < width; col++)
        {
            // BGRA
            imgdata[row * stride + col * 4 + 0] = 0;
            imgdata[row * stride + col * 4 + 1] = Convert.ToByte((1 - (col / (float)width)) * 0xff);
            imgdata[row * stride + col * 4 + 2] = Convert.ToByte((col / (float)width) * 0xff);
            imgdata[row * stride + col * 4 + 3] = Convert.ToByte((row / (float)height) * 0xff);
        }
    }
    var gradient = BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgra32, null, imgdata, stride);
    

    编辑:显然,您正在尝试使用 WPF 来制作某种图像编辑器。我仍将 WPF 原语用于形状和源位图,然后将平移、缩放、旋转实现为RenderTransform,位图效果实现为Effect,并将所有内容都保留在 WPF 模型中。但是,如果这对您不起作用,我们还有许多其他选择。

    您可以使用 WPF 基元渲染到 RenderTargetBitmap,该 PixelFormat 已选择与 WritableBitmap 一起使用,如下所示:

    Canvas cvRoot = new Canvas();
    // position primitives on canvas
    
    var rtb = new RenderTargetBitmap(width, height, dpix, dpiy, PixelFormats.Bgra32);
    var wb = new WritableBitmap(rtb);
    

    您可以使用 WPF DrawingVisual 发出 GDI 样式命令,然后呈现为位图,如 RenderTargetBitmap 页面上的示例所示。

    您可以使用通过System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmapHBITMAP 创建的InteropBitmap 使用GDI,该Bitmap.GetHBitmap 方法检索到。不过,请确保不要泄露 HBITMAP

    【讨论】:

    • 感谢您的回答,我一直在尝试测试您的代码,但我必须使用 Pbgra32 格式,并且我尝试让您的转换代码正常工作,但由于我的存储为 WriteableBitmap 而不是一个 BitmapSource 我无法让它工作。你知道我可以将 WriteableBitmap 转换为 Bgra32 吗?
    • @leaf68, WritableBitmap 继承自 BitmapSource,您可以使用 FormatConvertedBitmap 类在像素格式之间进行转换,但同样,这不是必需的。你能解释一下你想要做什么吗? PBGRA 通常仅用于存储图像,而不用于编辑或创建。
    • 我正在使用 WriteableBitmapEx 库来绘制形状,我刚刚发现它只接受 Pbgra 作为输入。嗯,是否可以像 Bgra 一样编辑 Pbgra 的原始数据,因为它的 alpha 值已预先混合到颜色中?
    • @leaf68,您可以使用与上面相同的代码来编辑 PBGRA,假设您考虑了不同格式所需的值。话虽如此,您不必使用像素操作来绘制形状。要么使用类原语,如RectanglePolygon 等...、DrawingContext,或者你甚至可以在紧要关头使用 GDI。您使用WritableBitmapEx 有什么原因吗?
    • 我正在使用 WriteableBitmapEx 绘制形状,并直接对图像处理程序的过滤器进行像素处理。我该如何处理 Pbgra32,因为我尝试了几个不同的值,但我无法弄清楚 alpha 通道的模式?有没有办法在 WPF 中使用 GDI?
    【解决方案2】:

    在长时间的头痛之后,我找到了this article,它解释了一种不使用位算术的方法,并允许我将其视为一个数组:

    unsafe
    {
       IntPtr pBackBuffer = bitmap.BackBuffer;
    
       byte* pBuff = (byte*)pBackBuffer.ToPointer();
    
       pBuff[4 * x + (y * bitmap.BackBufferStride)] = 255;
       pBuff[4 * x + (y * bitmap.BackBufferStride) + 1] = 255;
       pBuff[4 * x + (y * bitmap.BackBufferStride) + 2] = 255;
       pBuff[4 * x + (y * bitmap.BackBufferStride) + 3] = 255;
    }
    

    【讨论】:

      【解决方案3】:

      您可以通过调用Lock() 方法并在之后使用BackBuffer 属性来访问原始像素数据。完成后,别忘了致电AddDirtyRectUnlock。 举个简单的例子,你可以看看这个:http://cscore.codeplex.com/SourceControl/latest#CSCore.Visualization/WPF/Utils/PixelManipulationBitmap.cs

      【讨论】:

      • 谢谢!我使用您的链接中的*((int*)pBackBuffer) = color_data;(连同其他代码)来设置它。但是有一个问题:我怎样才能以类似的方式阅读它?
      • 另外,如何设置 alpha 值?
      • @leaf68,您正在使用指针算法直接访问位图,因此您读取它的方式与写入它的方式相同(color_data = *((int*)pBackBuffer))。在 ARGB 格式中,MSB 保存 8 位 alpha 值。您可以通过位运算来设置它。例如:currentValue &amp; 0x00ffffff | (newAValue &lt;&lt; 24) 会将其设置为任意字节。有关不安全代码的更多信息,请参阅 MSDN msdn.microsoft.com/en-us/library/aa664774(v=vs.71).aspx
      • 这取决于您使用的像素格式。但是您最终将使用位算术(如 Mitch 已经说过的)来组成所需的值。 @leaf68 顺便说一句:不要忘记将其标记为正确答案,以便其他用户尽快找到解决方案。