Monday, September 12, 2011

InteropBitmap in WPF

Ahoy ahoy,

Recently I found myself in a situation where I wanted to modify a bitmap on the fly and see the results immediately in WPF. Now as most WPF peeps know, to display an image in WPF is relatively straightforward. I've included a sample application that displays 2 tabs. One with an image that is just data-bound normally to a BitmapSource and another that displays an InteropBitmap in which we will regularly modify some pixels. You can download the entire VS solution as you wish. I'll only include the relevant sections of the code here for readability's sake.


Model.cs
public class Model : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void BroadcastPropertyChanged(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}

private BitmapSource beforeImage;
public BitmapSource BeforeImage
{
get { return beforeImage; }
set
{
if (beforeImage != value)
{
beforeImage = value;
BroadcastPropertyChanged("BeforeImage");
OnBeforeImageChanged();
}
}
}

private void OnBeforeImageChanged()
{
}
}


MainWindow.xaml.cs
public partial class MainWindow : UserControl
{
public MainWindow()
{
InitializeComponent();

Model m = new Model();
this.DataContext = m;
m.BeforeImage = new BitmapImage(new Uri(@"C:\Users\Coco\Pics\001.jpg"));
}
}


- INotifyPropertyChanged is in the System.ComponentModel namespace (System.dll)
- BitmapSource is in the System.Windows.Media.Imaging namespace (PresentationCore.dll)


If I run the application I see the image in my Before tab displayed properly. Nothing is in the After tab because we have yet to create a property "AfterImage" in the Model class.




Barcelona is great, isn't it? However, it is beside the fact. To display an image we bind the Image.Source property to either a BitmapSource or a string (giving us the path of the image to display). In this case we'll just stick to the BitmapSource.


The BitmapImage (which inherits from BitmapSource) that we set as our BeforeImage does not expose any methods to edit pixels or write into them. There are two implementations (probably amongst others) of BitmapSource that allow us to do just that. Choosing the right one depends on the circumstance.

  • WriteableBitmap
  • InteropBitmap

In my case, I chose InteropBitmap (reasons to follow) and that will be the focus of the article. InteropBitmap was originally created to render Windows Forms Bitmaps in a WPF environment. But that is not why I used it. I required the ability to have direct access to the memory buffer used to store the pixels of the image and wanted the ability to modify these pixels (slots in the memory buffer) from any thread (a normal BitmapImage allows access to it only from within the thread that created it). InteropBitmap provides this and when a BitmapSource is asked for by the Image control, we call Invalidate on the InteropBitmap and return a frozen frame of the current state of the memory buffer. If this isn't clear, just read on, hopefully it will soon make more sense.


Proper use of the InteropBitmap requires us to first expose some Win32 methods to assist us in its creation. We'll use these Win32 methods to create a section of memory that we can later write to directly as our image. The code below belongs in the Model class


Model.cs
#region Extern Stuff
const uint FILE_MAP_ALL_ACCESS = 0xF001F;
const uint PAGE_READWRITE = 0x04;

[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr CreateFileMapping(IntPtr hFile,
IntPtr lpFileMappingAttributes,
uint flProtect,
uint dwMaximumSizeHigh,
uint dwMaximumSizeLow,
string lpName);

[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject,
uint dwDesiredAccess,
uint dwFileOffsetHigh,
uint dwFileOffsetLow,
uint dwNumberOfBytesToMap);
#endregion

private int bufferWidth = 0;
private int bufferHeight = 0;
private uint bufferByteCount = 0;

private InteropBitmap outputBitmap;
private IntPtr outputSectionPointer;
private IntPtr outputMapPointer;
private static readonly PixelFormat outputFormat = PixelFormats.Bgra32;

private void OnBeforeImageChanged()
{
if (bufferWidth != BeforeImage.PixelWidth ||
bufferHeight != BeforeImage.PixelHeight)
{
// record the new image dimensions
bufferWidth = BeforeImage.PixelWidth;
bufferHeight = BeforeImage.PixelHeight;
bufferByteCount = (uint)(bufferWidth * bufferHeight * ((outputFormat.BitsPerPixel + 7) / 8));

outputSectionPointer = CreateFileMapping(new IntPtr(-1), IntPtr.Zero, PAGE_READWRITE, 0, bufferByteCount, null);

outputMapPointer = MapViewOfFile(outputSectionPointer, FILE_MAP_ALL_ACCESS, 0, 0, bufferByteCount);
}

// copy the original image into the buffer we created for the after image
BeforeImage.CopyPixels(new Int32Rect(0, 0, bufferWidth, bufferHeight), outputMapPointer, (int)bufferByteCount, bufferWidth * 4);

// build the InteropBitmap from the memory buffer
AfterImage = Imaging.CreateBitmapSourceFromMemorySection( outputSectionPointer, bufferWidth, bufferHeight,
outputFormat, bufferWidth * 4, 0) as InteropBitmap;
}

public BitmapSource AfterImage
{
get
{
if (outputBitmap != null)
{
outputBitmap.Invalidate();
return (BitmapSource)outputBitmap.GetAsFrozen();
}

return null;
}
private set
{
outputBitmap = value as InteropBitmap;
BroadcastPropertyChanged("AfterImage");
}
}


Once we are done creating the InteropBitmap and its memory buffer, we can write to the buffer and broadcast the AfterImage PropertyChanged event so that the (data-bound) Image viewer in the After tab will update itself. The MainWindow now contains buttons to start and stop the writing to random pixels. It also contains a refresh frequency textbox which will indicate how often we want to update the UI. Be careful though, too many UI updates and you'll bog down the UI thread. This is because when we broadcast the PropertyChanged event, the databound Image viewer in the UI listens to the event (internally) and gets the Model.AfterImage property. This forces a render cycle on the InteropBitmap and then a clone of the InteropBitmap's buffer is returned. So if you're dealing with big buffers, you may start to experience lagging. So try to limit your PropertyChanged event broadcasts on the image property exposed in the model (AfterImage in our case).


The code below was used to modify random pixels in the image's memory buffer.


Model.cs
private DateTime lastImgRefreshTime = DateTime.MinValue;
private int imageRefreshFreq = 500;
private bool isWritingPixels;
private ManualResetEvent mre;

public Model()
{
isWritingPixels = false;
mre = new ManualResetEvent(isWritingPixels);
Task.Factory.StartNew(new Action(() =>
{
Random rand = new Random();
while (true)
{
// wait for the user to start the pixel writing
mre.WaitOne();

unsafe
{
Int32* pStart = (Int32*)outputMapPointer;

int x = rand.Next(bufferWidth);
int y = rand.Next(bufferHeight);

pStart[y * this.bufferWidth + x] = unchecked((int)0xFF000000) | rand.Next();
}

DateTime now = DateTime.Now;
if ((now - lastImgRefreshTime).TotalMilliseconds > imageRefreshFreq)
{
BroadcastPropertyChanged("AfterImage");
lastImgRefreshTime = now;
}
}
}));
}

internal void StartPixelWriting()
{
TogglePixelWriting();
}

private void TogglePixelWriting()
{
if (isWritingPixels)
{
mre.Reset();
}
else
{
mre.Set();
}

isWritingPixels = !isWritingPixels;
BroadcastPropertyChanged("CanStartWriting");
BroadcastPropertyChanged("CanStopWriting");
}

internal void StopPixelWriting()
{
TogglePixelWriting();
}


And now you see the end result.



No comments:

Post a Comment