Edit raw pixel data of WriteableBitmap?
Asked Answered
P

3

10

Is it possible to directly read/write to a WriteableBitmap's pixel data? I'm currently using WriteableBitmapEx's SetPixel() but it's slow and I want to access the pixels directly without any overhead.

I haven't used HTML5's canvas in a while, but if I recall correctly you could get its image data as a single array of numbers and that's kind of what I'm looking for

Thanks in advance

Pantry answered 24/11, 2013 at 21:16 Comment(0)
V
34

To answer your question, you can more directly access a writable bitmap's data by using the Lock, write, Unlock pattern, as demonstrated below, but it is typically not necessary unless you are basing your drawing upon the contents of the image. More typically, you can just create a new buffer and make it a bitmap, rather than the other way around.

That being said, there are many extensibility points in WPF to perform innovative drawing without resorting to pixel manipulation. For most controls, the existing WPF primitives (Border, Line, Rectangle, Image, etc...) are more than sufficient - don't be concerned about using many of them, they are rather cheap to use. For complex controls, you can use the DrawingContext to draw D3D primitives. For image effects, you can implement GPU assisted shaders using the Effect class or use the built in effects (Blur and Shadow).

But, if your situation requires direct pixel access, pick a pixel format and start writing. I suggest BGRA32 because it is easy to understand and is probably the most common one to be discussed.

BGRA32 means the pixel data is stored in memory as 4 bytes representing the blue, green, red, and alpha channels of an image, in that order. It is convenient because each pixel ends up on a 4 byte boundary, lending it to storage in an 32 bit integer. When dealing with a 32 bit integer, keep in mind the order will be reversed on most platforms (check BitConverter.IsLittleEndian to determine proper byte order at runtime if you need to support multiple platforms, x86 and x86_64 are both little endian)

The image data is stored in horizontal strips which are one stride wide which compose a single row the width of an image. The stride width is always greater than or equal to the pixel width of the image multiplied by the number of bytes per pixel in the format selected. Certain situations can cause the stride to be longer than the width * bytesPerPixel which are specific to certain architechtures, so you must use the stride width to calculate the start of a row, rather than multiplying the width. Since we are using a 4 byte wide pixel format, our stride does happen to be width * 4, but you should not rely upon it.

As mentioned, the only case I would suggest using a WritableBitmap is if you are accessing an existing image, so that is the example below:

Before / After:

image run through below algorithm

// 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();

However, it really isn't necessary if you are not modifying an existing image. For example, you can draw a checkerboard pattern using all safe code:

Output:

checkerboard pattern at 25 percent zoom

// 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);

And you don't really even need an Int32 intermediate, if you write to the byte array directly.

Output:

Gradient produced from below

// 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);

Edit: apparently, you are trying to use WPF to make some sort of image editor. I would still be using WPF primitives for shapes and source bitmaps, and then implement translations, scaling, rotation as RenderTransform's, bitmap effects as Effect's and keep everything within the WPF model. But, if that does not work for you, we have many other options.

You could use WPF primitives to render to a RenderTargetBitmap which has a chosen PixelFormat to use with WritableBitmap as below:

Canvas cvRoot = new Canvas();
// position primitives on canvas

var rtb = new RenderTargetBitmap(width, height, dpix, dpiy, PixelFormats.Bgra32);
var wb = new WritableBitmap(rtb);

You could use a WPF DrawingVisual to issue GDI style commands then render to a bitmap as demonstrated on the sample on the RenderTargetBitmap page.

You could use GDI using an InteropBitmap created using System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap from an HBITMAP retrieved from a Bitmap.GetHBitmap method. Make sure you don't leak the HBITMAP, though.

Vulgarity answered 25/11, 2013 at 0:12 Comment(8)
Thanks for the answer, I've been trying test out your code but I have to use the Pbgra32 format, and I tried getting your conversion code to work but since mine is stored as a WriteableBitmap and not a BitmapSource I couldn't get it to work. Do you know hwo I can convert a WriteableBitmap to Bgra32?Pantry
@leaf68, WritableBitmap inherits from BitmapSource and you can convert between pixelformats using the FormatConvertedBitmap class, but again, it should not be necessary. Can you explain what you are trying to do? PBGRA is typically only used in storage of images, not in editing or creation.Vulgarity
I'm using the WriteableBitmapEx library for drawing shapes, and I just found out it only accepts Pbgra as an input. Hmm, is it possible to edit Pbgra's raw data similarly to Bgra, since it's alpha value is pre-mixed into the colors?Pantry
@leaf68, you can use the same code as above to edit PBGRA, assuming you take into account the values needed for the different format. That being said, you shouldn't have to use pixel manipulation to draw shapes. Either use class primitives like Rectangle, Polygon, etc..., DrawingContext, or you could even use GDI in a pinch. Is there some reason you are using WritableBitmapEx?Vulgarity
I'm using WriteableBitmapEx to draw shapes, and direct pixel manipulation for filters for an image manipulating program. How can I handle Pbgra32, because I tried several different values and I couldn't figure out the pattern for the alpha channel? Is there a way to use GDI in WPF?Pantry
@leaf68, see the edit for ways to avoid using PBGRA. For reference, though, PBGRA is pre-multiplied, so to adjust an alpha value, you need to multiply the RGB levels as well. See the Wikipedia article for more on PBGRA (en.wikipedia.org/wiki/Alpha_compositing)Vulgarity
Sorry for the late response, got busy with school work. I ended up using your conversion doe to convert between Pbgra32 for WriteableBitmapEx and Bgra32 for raw pixel manipulation, and I was hesitant to do this but I did some benchmarks and it's a very quick conversion and doesn't cause a noticable impact on performance. Thanks!Pantry
@leaf68, if this answers your question, you should mark it as answer.Vulgarity
P
6

After a nice long headache, I found this article that explains a way to do it without using bit arithmetic, and allows me to treat it as an array instead:

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;
}
Pantry answered 25/11, 2013 at 0:16 Comment(0)
B
1

You can access the raw pixel data by calling the Lock() method and using the BackBuffer property afterwards. When you're finished, don't forget to call AddDirtyRect and Unlock. For a simple example, you can take a look at this: http://cscore.codeplex.com/SourceControl/latest#CSCore.Visualization/WPF/Utils/PixelManipulationBitmap.cs

Balalaika answered 24/11, 2013 at 21:35 Comment(4)
Thanks! I used *((int*)pBackBuffer) = color_data; from your link (along with the other code) to set it. But one question: how can I read it similarly?Pantry
And also, how can I set the alpha value?Pantry
@leaf68, you are accessing the bitmap directly using pointer arithmetic, so you read it the same way you write it (color_data = *((int*)pBackBuffer)). In an ARGB format, the MSB holds an 8 bit alpha value. You can set it by doing bit arithmetic. E.g.: currentValue & 0x00ffffff | (newAValue << 24) would set it to an arbitrary byte. See MSDN For more on unsafe code msdn.microsoft.com/en-us/library/aa664774(v=vs.71).aspx.Vulgarity
It depends on the pixelformat you are using. But you will end up in using bit arithmetic(like Mitch already said) to compose the desired values. @leaf68 Btw: Don't forget to mark it as the correct answer so other users can find the solution as fast as possible.Balalaika

© 2022 - 2024 — McMap. All rights reserved.