Monday, June 29, 2009

Bitmap cloning is not Bitmap cloning

Almost 7 years ago, I had a pet project called 'PBrush', a small paint program created in VB 6. It was where I experimented with implementing the Undo/Redo features, image processing (emboss/invert/edge-detection, etc) in addition to the standard tools found in MSPaint.
I lost the project files when my first RAID-0 array broke and I've forgotten about it since then. (Since then I don't keep my project files on a raid-0 array.)

So, here I am today trying to get back in touch with VB.net after quite some time working in the Java/JSP platform and out of nowhere, I am reminded of the old PBrush project of mine. Not surprising really, since I had the most fun working on that project, ya know, trying different things with pixels and coming up with names for them :) Good times.

So anyway, I downloaded VB.net 2008 express edition and began coding about a few days ago in my spare times. I've already got a couple of kernel-based filters for edge-detection (Sobel. Scharr) coded down using basic loops for now. I heard directx methods will be faster for this, so I might try to re-write these functions.

For now, I am using a PictureBox for the main display and pass its Image property to my functions that will do the image processing. I had it all working in a day. But, as these filters are time-consuming, I noticed that the form would go to 'Not Responding' state after about 3 seconds of processing. This turns out to be caused by the form and its child controls staying "invalided" for too long. That is, the controls want to re-paint themselves, but can't because of the current thread is busy executing the image proessing routine.
First I tried the good old Application.doEvents()
Turns out that this method is old alright but certainly not "good". Some call for its timely death, even, and I can see their point. After adding this method call, my functions were taking about ten times more time to complete. Online documentations say that this is expected behaviour(!). Called 're-entrancy', this function halts any method that's been running, processes form messages and, get this, re-enters the function that it interupted.

Well, I said "no thank you. Don't come again" and started looking into putting the functions in separate threads instead.

Now, this is my first (not counting lab projects) project with threading. But it turns out that putting a function call into a separate thread is too easy. I used the BackgroundWorker class for doing this. Now, the functions work quite faster and the UI doesn't stay "invalidated" either. Seems perfect on the surface. But the devil is in the details, yes?

Yes. To better illustrate this point, let me give you a brief overview of the code.
So, I have a Picture box that holds the bitmap that needs processing. This control is also the one that is visible to the user, so it has to stay "validated". When an image filter is chosen by clicking on a menu item, I pass the Image object of the PictureBox to the appropriate function to be processed in the thread's DoWork method. Like so:

Dim bmpSrc as Bitmap
bmpSrc = pbMainDisplay.Image
e.Result = pdc.applyFilter(bmpSrc)


I noticed that "sometimes", when the BackgroundWorker is executing an image processing routine in the background, the Picture box will show a big red 'X' on it instead of the bitmap and an exception will be thrown stating "Bitmap region is already locked." on the line where I try to do a GetPixel().

I thought that it was simply a matter of adding a clone() method call on the Image property as this would create a new Image object instead of passing the reference to the Image. When this didn't work, I searched for every usage of the bitmap on the code and added the Clone() method call to it. Finally, when all the Image properties were adorned with the Clone() method, I ran the project again.... and met with the same problem, unchanged.

I looked up this issue online and found a similarly frustrated developer's blog entry on this. I gladly gave his method a try, like this:

Dim bmpSrc as Bitmap
bmpSrc = New Bitmap(pbMainDisplay.Image)
e.Result = pdc.applyFilter(taMain).Clone()


Unfortunately, this approach didn't work for me either.
Frustrated, I tried to either set the visibility of the Picture Box to false whenever the picture is being worked on by the background thread. But this would make the picture not visible to the user, which is ultimately not very "smooth".
So, for now, I have opted to intercept the paint events of the picture box and disallowing it whenever the background thread is busy, like so:


Private Sub pbMainDisplay_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles pbMainDisplay.Invalidated
If bwMain.IsBusy() Then
Exit Sub
End If
End Sub

It might be a workaround, but it's the only thing I've tried that works.


Update: 8th July, 2009

I have since moved the code that sets the Bitmap variable from the background worker's DoWork event to the method that calls the background worker instance's RunWorkerAsync() method, et voila, no stinkin workarounds required now.
It had been a matter of inter-thread chatter, I suppose, between the main application thread and the background worker.