Parallelizing GDI+ Image Resizing .net

Solution 1:

Use Processes.

GDI+ blocks per process a lot of ways. Yep, a pain, but there's no way around it. Fortunately with tasks like this one (and any one that processes files on the filesystem), it's too easy to just split the workload up between multiple processes. Fortunately it looks like GDI+ is using locks, not mutex, so it is concurrent!

We have some graphics programs where I work to do image processing. One programmer starts 6-7 copies at once of a conversion program, in production. So it is not messy, trust me. Hack? You're not getting paid to look pretty. Get the job done!

Cheap Example (note this will NOT work in the ide, build it and run the EXE):

Imports System.Drawing
Module Module1
    Dim CPUs As Integer = Environment.ProcessorCount

    Dim pRuns As New System.Collections.Generic.List(Of Process)

    Sub Main()
        Dim ts As Date = Now
        Try
            If Environment.GetCommandLineArgs.Length > 1 Then
                LongTerm(Environment.GetCommandLineArgs(1))
                Exit Sub
            End If

            Dim i As Integer = 0
            Dim files As String() = IO.Directory.GetFiles("D:\TEMP", "*.jpg")
            Dim MAX As Integer = Math.Min(26, files.Count)
            While pRuns.Count > 0 Or i < MAX

                System.Threading.Thread.Sleep(100)

                If pRuns.Count < CInt(CPUs * 1.5) And i < MAX Then ''// x2 = assume I/O has low CPU load
                    Console.WriteLine("Starting process pRuns.count = " & pRuns.Count & " for " & files(i) & " path " & _
                                        Environment.GetCommandLineArgs(0))
                    Dim p As Process = Process.Start(Environment.GetCommandLineArgs(0), """" & files(i) & """")
                    pRuns.Add(p)
                    i += 1
                End If

                Dim i2 As Integer
                i2 = 0
                While i2 < pRuns.Count
                    If pRuns(i2).HasExited Then
                        pRuns.RemoveAt(i2)
                    End If
                    i2 += 1
                End While


            End While
        Catch ex As Exception
            Console.WriteLine("Blew up." & ex.ToString)
        End Try
        Console.WriteLine("Done, press enter. " & Now.Subtract(ts).TotalMilliseconds)
        Console.ReadLine()
    End Sub

    
    Sub LongTerm(ByVal file As String)
        Try
            Dim newImageHeight As Integer
            Dim oldImage As Image
            Console.WriteLine("Reading " & CStr(file))
            oldImage = Image.FromFile(CStr(file))
            Dim rect As Rectangle

            newImageHeight = Convert.ToInt32(850 * oldImage.Height / oldImage.Width)
            Using newImage As New Bitmap(850, newImageHeight, oldImage.PixelFormat)
                Using graph As Graphics = Graphics.FromImage(newImage)
                    rect = New Rectangle(0, 0, 850, newImageHeight)

                    With graph
                        .CompositingQuality = Drawing2D.CompositingQuality.HighQuality
                        .SmoothingMode = Drawing2D.SmoothingMode.HighQuality
                        .InterpolationMode = Drawing2D.InterpolationMode.HighQualityBicubic
                    End With

                    Console.WriteLine("Converting " & CStr(file))
                    graph.DrawImage(oldImage, rect)

                    Console.WriteLine("Saving " & CStr(file))
                    newImage.Save("d:\temp\Resized\" & _
                                  IO.Path.GetFileNameWithoutExtension(CStr(file)) & ".JPG", _
                                   System.Drawing.Imaging.ImageFormat.Jpeg)
                End Using
            End Using
        Catch ex As Exception
            Console.WriteLine("Blew up on  " & CStr(file) & vbCrLf & ex.ToString)
            Console.WriteLine("Press enter")
            Console.ReadLine()
        End Try
    End Sub

End Module

Solution 2:

I am not sure why execution of Graphics.DrawImage seems to serialize for you, but I actually noticed a race condition with your general pattern of queuing the work items. The race is between the WaitOne and the Set. It is possible for the first work item to Set before any of the others have even been queued yet. That will cause WaitOne to return immediately before all work items have completed.

The solution is to treat the main thread as if it were a work item. Increment pRuns once before queueing begins and then decrement and signal the wait handle after queueing is complete just as you would in a normal work item. However, the better approach is to use the CountdownEvent class if that is available to you as it simplifies the code. As luck would have it I just recently posted the pattern in another question.

Solution 3:

If you don't mind a WPF approach, here is something to try. The following is a simple rescale method that accepts image streams and produces a byte[] containing the resulting JPEG data. Since you do not want to actually draw the images with GDI+, I thought this was suitable for you despite being WPF-based. (The only requirement is to reference WindowsBase and PresentationCore in your project.)

Advantages include faster encoding (by 200-300% on my machine) and better parallel speedup, although I also see some unwanted serialization in the WPF rendering path. Let me know how this works for you. I'm sure it could be optimized further if necessary.

The code:

 byte[] ResizeImage(Stream source)
 {
    BitmapFrame frame = BitmapFrame.Create(source, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.None);
    var newWidth = frame.PixelWidth >> 1;
    var newHeight = frame.PixelHeight >> 1;
    var rect = new Rect(new System.Windows.Size(newWidth, newHeight));
    var drawingVisual = new DrawingVisual();
    using (var drawingContext = drawingVisual.RenderOpen())
        drawingContext.DrawImage(frame, rect);
    var resizedImage = new RenderTargetBitmap(newWidth, newHeight, 96.0, 96.0, PixelFormats.Default);
    resizedImage.Render(drawingVisual);
    frame = BitmapFrame.Create(resizedImage);

    using (var ms = new MemoryStream())
    {
        var encoder = new JpegBitmapEncoder();
        encoder.Frames.Add(frame);
        encoder.Save(ms);
        return ms.ToArray();
    }
 }

Solution 4:

Use a image-processing library other than GDI+.

We use ImageMagick at a pretty high-volume web site it resize uploaded images (the uploaded images are usually 10-40 MPixels but to be able to work with them in the website (in Flash modules), we resize them to have smallest size 1500 pixels). Processing is pretty fast and gives excellent results.

We currently start a new ImageMagick process using the command-line interface. This gives some overhead when starting new processes, but since the images are so large it is usually a pretty small time slice of the total resizing process. It is also possible to use ImageMagick in-process but have haven't tried that yet since it 1. we do not need the extra performance it gives and 2. it feels good to run third-party software in other processes.

When using ImageMagick, you also get a number of other possibilities like better filtering and lots of other functions.