Wednesday, December 14, 2011

JpegBitmapEncoder and TLP finally got married

Well, just out of pure need of performance I wanted to re-write the previously mentioned test with JpegBitmapEncoder using TPL.

Bad luck for me. I got an InvalidOperationException. I thought I messed something up and didn't want to dive further. But recently I had a need to process a large amount of jpegs and got back to TPL.

Bad luck for me. As I figured out, image processing in WPF is actually Windows Imaging Component wrapped around, not a pure .Net code. Most of the work is hidden behind COM interfaces. I made a separate project for testing WPF's imaging and threading. I found different questions on stackoverflow (WPF/BackgroundWorker and BitmapSource problem and How to copy DispatcherObject (BitmapSource) into different thread?), something worthless on social.msdn, but no solution.

I launched Reflector to look under the hood, no idea.

I don't know how exactly I did deduce the solution, but it's pretty trivial: wrap your BitmapSource with WriteableBitmap.

Here is the code:

#define USE_DIRTY_TRICK_WITH_WRITEABLE_BITMAP
//#define USE_DIRECT_CALL
#define USE_THREADING
//#define USE_TPL

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Windows.Media.Imaging;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;


class Program
{
    static Process p = Process.GetCurrentProcess();

    const string path = @"Q:\"; // a folder with pictures

    static byte[] CompressBitmapSource(BitmapSource bitmapSource)
    {
#if USE_DIRTY_TRICK_WITH_WRITEABLE_BITMAP
        {
            var t1 = p.TotalProcessorTime;

            // this changes everything
            bitmapSource = new WriteableBitmap(bitmapSource);
            // in this case, new WriteableBitmap(bitmapSource) is even called in a thread that is
            // different from the one bitmapSource was created in.

            var t2 = p.TotalProcessorTime;
            // How much did we 'pay' for the conversion?
            Console.WriteLine("Converting to WriteableBitmap: {0:F4} ms", (t2 - t1).TotalMilliseconds);
        }
#endif

        byte[] buffer;
        {
            var t1 = p.TotalProcessorTime;

            var enc = new JpegBitmapEncoder();
            enc.QualityLevel = 50; // some value

            var bf = BitmapFrame.Create(bitmapSource); // will throw InvalidOperationException (without WriteableBitmap trick)
            enc.Frames.Add(bf); 

            var ms = new MemoryStream();
            enc.Save(ms);
            buffer = ms.ToArray();

            var t2 = p.TotalProcessorTime;
            Console.WriteLine("Encoding {0}x{1}: {2:F4} ms", bitmapSource.PixelWidth, bitmapSource.PixelHeight, (t2 - t1).TotalMilliseconds);
        }
        return buffer;
    }

    static BitmapFrame BitmapFrameFromFile(string filename)
    {
        var t1 = p.TotalProcessorTime;
        
        BitmapFrame res = null;
        using (var s = File.OpenRead(filename))
        {
            // BitmapCacheOption.OnLoad must be there since s -- s stream -- will be disposed
            res = BitmapFrame.Create(s, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
        }
        //res.Freeze(); // not obligatory

        var t2 = p.TotalProcessorTime;

        Console.WriteLine("Loading: {0:F4} ms", (t2 - t1).TotalMilliseconds);
        return res;
    }

    void Run()
    {
        var jpgs = Directory.GetFiles(path, "*.jpg");

        foreach (var file in jpgs.Take(10))
        {
            var bf = BitmapFrameFromFile(file);

            byte[] bytes = new byte[0];
#if USE_DIRECT_CALL
            bytes = CompressBitmapFrame(bf);
#endif

#if USE_THREADING
            Thread th = new Thread((ThreadStart)(() =>
                {
                    bytes = CompressBitmapSource(bf);
                }));
            //th.SetApartmentState(ApartmentState.STA); // not obligatory
            th.Start();
            th.Join();
#endif

#if USE_TPL
            var task = new Task(() => bytes = CompressBitmapSource(bf));
            task.Start();
            task.Wait();
#endif

            Console.WriteLine("{0} length = {1} bytes when compressed.", file, bytes.Length);
        }
    }

    //[STAThread] // not obligatory
    static void Main(string[] args)
    {
        new Program().Run();

        if (Debugger.IsAttached)
        {
            Console.WriteLine("Press [Enter] to exit...");
            Console.ReadLine();
        }
    }
}

Here is the sample output:
Loading: 624,0040 ms
Converting to WriteableBitmap: 93,6006 ms
Encoding 4608x3456: 218,4014 ms
Q:\DSC04935.JPG length = 849378 bytes when compressed.
Loading: 639,6041 ms
Converting to WriteableBitmap: 46,8003 ms
Encoding 4608x3456: 249,6016 ms
Q:\DSC04936.JPG length = 1129534 bytes when compressed.
Loading: 624,0040 ms
Converting to WriteableBitmap: 62,4004 ms
Encoding 4608x3456: 218,4014 ms
Q:\DSC04937.JPG length = 737203 bytes when compressed.
Loading: 639,6041 ms
Converting to WriteableBitmap: 46,8003 ms
Encoding 4608x3456: 249,6016 ms
Q:\DSC04938.JPG length = 896944 bytes when compressed.
Loading: 608,4039 ms
Converting to WriteableBitmap: 46,8003 ms
Encoding 4608x3456: 234,0015 ms
Q:\DSC04939.JPG length = 1010188 bytes when compressed.
Loading: 608,4039 ms
Converting to WriteableBitmap: 31,2002 ms
Encoding 4608x3456: 234,0015 ms
Q:\DSC04940.JPG length = 709967 bytes when compressed.
Loading: 530,4034 ms
Converting to WriteableBitmap: 46,8003 ms
Encoding 4608x3456: 187,2012 ms
Q:\DSC04941.JPG length = 414621 bytes when compressed.
Loading: 561,6036 ms
Converting to WriteableBitmap: 31,2002 ms
Encoding 4608x3456: 249,6016 ms
Q:\DSC04942.JPG length = 610337 bytes when compressed.
Loading: 530,4034 ms
Converting to WriteableBitmap: 46,8003 ms
Encoding 4608x3456: 218,4014 ms
Q:\DSC04943.JPG length = 648009 bytes when compressed.
Loading: 546,0035 ms
Converting to WriteableBitmap: 46,8003 ms
Encoding 4608x3456: 202,8013 ms
Q:\DSC04944.JPG length = 727393 bytes when compressed.
Press any key to continue . . .

Happy coding!
P.S. I didn't look of what happens to metadata.

No comments:

Post a Comment