Monday, 17 June 2013

Accessing Image Pixel Data in a C# Windows Store App

Previously I’ve written about how to access image pixel data in a C++/CX Windows Store App. Depending on your app, you may have a requirement to perform imaging operations that manipulate the pixel values of an image. Rather than have an Image control display a BitmapImage, I’ve modified my PhotoViewer sample app to display a WriteableBitmap instead, allowing the manipulation of an image at the pixel level. This should be a useful blog post as many of the posts concerning this topic on the forums are not optimal.

In the sample app, when the PhotoPage is loaded, the OnNavigatedTo method is called, which in turn calls the LoadPhoto method.

private async void LoadPhoto()
{
    var file = await _repository.GetPhotoAsync(_filePath);
    var imageData = await file.OpenReadAsync();
    var decoder = await BitmapDecoder.CreateAsync(imageData);
 
    _width = Convert.ToInt32(decoder.PixelWidth);
    _height = Convert.ToInt32(decoder.PixelHeight);
    _bmp = new WriteableBitmap(_width, _height);
 
    imageData.Seek(0);
    await _bmp.SetSourceAsync(imageData);
    Photo = _bmp;
    OnPropertyChanged("Photo");
}

This method simply loads the photo from disk, and decodes it for display in a WriteableBitmap. The pixel data in a WriteableBitmap is stored in the PixelBuffer property. However, this property is read-only. In managed languages, you can use the AsStream extension method (from the System.Runtime.InteropServices.WindowsRuntime namespace) to access the underlying buffer as a stream. To see how to access the underlying buffer in C++/CX, see this blog post.

// Get source pixel data
var srcPixelStream = _bmp.PixelBuffer.AsStream();
byte[] srcPixels = new byte[4 * _width * _height];
int length = srcPixelStream.Read(srcPixels, 0, 4 * _width * _height);
 
// Convert to greyscale
byte[] destPixels = Imaging.ToGreyscale(srcPixels, _width, _height);

The code above accesses the underlying buffer in the WriteableBitmap as a stream, creates a new byte array to store the pixel data, and then reads the pixel data from the stream to the byte array. Then the ToGreyscale method is called to convert the pixel data to greyscale.

public static byte[] ToGreyscale(byte[] srcPixels, int width, int height)
{
    byte b, g, r, a, luminance;
    byte[] destPixels = new byte[4 * width * height];
 
    // Convert pixel data to greyscale
    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            b = srcPixels[(x + y * width) * 4];
            g = srcPixels[(x + y * width) * 4 + 1];
            r = srcPixels[(x + y * width) * 4 + 2];
            a = srcPixels[(x + y * width) * 4 + 3];
            luminance = Convert.ToByte(0.299 * r + 0.587 * g + 0.114 * b);
 
            destPixels[(x + y * width) * 4] = luminance;     // B
            destPixels[(x + y * width) * 4 + 1] = luminance; // G
            destPixels[(x + y * width) * 4 + 2] = luminance; // R
            destPixels[(x + y * width) * 4 + 3] = luminance; // A
 
        }
    }
 
    return destPixels;
}

The ToGreyscale method iterates through the srcPixel byte array, converting each value to its equivalent greyscale representation, before returning it to the calling method. Note that the pixel data is stored in BGRA format.

// Write greyscale image back to WriteableBitmap
srcPixelStream.Seek(0, SeekOrigin.Begin);
srcPixelStream.Write(destPixels, 0, length);
_bmp.Invalidate();

Back in the calling method the stream that accessed the underlying buffer in the WriteableBitmap has its position reset to the beginning. Then the destPixels byte array, containing the greyscale representation of the image data, is written back to the stream, and hence to the underlying WriteableBitmap. A call is made to the WriteableBitmap.Invalidate method to request a redraw of the entire bitmap.

Summary

The pixel data in a WriteableBitmap is stored in the PixelBuffer property. However, this property is read-only. In managed languages, you can use the AsStream extension method (from the System.Runtime.InteropServices.WindowsRuntime namespace) to access the underlying buffer as a stream. You can then use the Read and Write methods on the stream to access the pixel data and replace it.

The sample app can be downloaded here.

1 comment:

  1. The real question is why the heck did they removed the Bitmap class, this was very easy to use and now we have to deal with dozens of messed up lines of code to change A SINGLE PIXEL in a little png file.

    But thanks a lot for your help !

    ReplyDelete