并行化GDI+图像调整大小 .net

19

我尝试使用 .Net 并行调整 jpeg 图像的大小,但是所有尝试都失败了,因为 Graphics.DrawImage 函数在活动时似乎会锁定。尝试以下代码片段:

Sub Main()
    Dim files As String() = IO.Directory.GetFiles("D:\TEMP")
    Dim imgs(25) As Image
    For i As Integer = 0 To 25
      imgs(i) = Image.FromFile(files(i))
    Next

    Console.WriteLine("Ready to proceed ")
    Console.ReadLine()

    pRuns = 1
    For i As Integer = 0 To 25
      Threading.Interlocked.Increment(pRuns)
      Threading.ThreadPool.QueueUserWorkItem(New Threading.WaitCallback(AddressOf LongTerm), imgs(i))
    Next
    Threading.Interlocked.Decrement(pRuns)

    pSema.WaitOne()
    Console.WriteLine("Fin")
    Console.ReadLine()
  End Sub

  Sub LongTerm(ByVal state As Object)
    Dim newImageHeight As Integer
    Dim oldImage As Image = CType(state, Image)
    Dim newImage As Image
    Dim graph As Graphics
    Dim rect As Rectangle
    Dim stream As New IO.MemoryStream

    Try
      newImageHeight = Convert.ToInt32(850 * oldImage.Height / oldImage.Width)
      newImage = New Bitmap(850, newImageHeight, oldImage.PixelFormat)
      graph = 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

      'Save image to memory stream
      graph.DrawImage(oldImage, rect)
      newImage.Save(stream, Imaging.ImageFormat.Jpeg)
    Catch ex As Exception

    Finally
      If graph IsNot Nothing Then
        graph.Dispose()
      End If
      If newImage IsNot Nothing Then
        newImage.Dispose()
      End If
      oldImage.Dispose()
      stream.Dispose()

      Console.WriteLine("JobDone {0} {1}", pRuns, Threading.Thread.CurrentThread.ManagedThreadId)
      Threading.Interlocked.Decrement(pRuns)
      If pRuns = 0 Then
        pSema.Set()
      End If
    End Try

  End Sub

所有线程都在graph.DrawImage()处等待。是否有其他函数可以加快代码性能?是否不可能使用Graphics.Draw同时使用多个线程?在实际应用程序中,应该同时调整多个图像的大小(在四核PC上),而不总是相同的。发布的代码仅供测试目的...

提前致谢

编辑:根据评论更新了代码


如果添加pSema和pRuns的声明,回答者就可以更轻松地复制粘贴并测试程序了。 - FastAl
我发现大部分时间都花费在IO上,而不是DrawImage调用上。如果你将流传递给Image构造函数,在解码器中就可以避免一些锁定问题(并且速度稍快)。当构建imageresizing.net库时,我进行了一些基准测试。 - Lilith River
此外,WIC在WS2008 R2和Win7上是一个不错的选择,而http://imageresizing.net/支持该代码路径作为一组插件。在服务器上使用WPF仍然是个坏主意,存在太多未修补的内存泄漏问题。 - Lilith River
4个回答

23

使用进程。

GDI +会阻止许多进程。是的,这很痛苦,但无可避免。幸运的是,对于像这样的任务(以及在文件系统上处理文件的任何任务),很容易只需将工作负载分割成多个进程。幸运的是,看起来GDI +正在使用锁,而不是互斥锁,因此它是并发的!

我们在工作中有一些图形程序用于图像处理。一位程序员在生产中同时启动6-7个转换程序副本。所以这不会变得凌乱,相信我。Hack?你不是为了好看而付费的。完成工作就行了!

廉价示例(请注意,这将无法在IDE中运行,请构建并运行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

2
我赞同这个建议。使用多线程进行进程内GDI操作,迟早会遇到死锁或完全随机的失败。在将一个示例升级到Microsoft PSS后,他们承认由于某些竞争条件而导致堆栈/堆的损坏,但没有真正的解决方案。如果想要并行化,请坚持使用进程外转换(或使用第三方解决方案,该解决方案是线程安全的)! - Kevin Pullin
1
当您完成进程时,难道不应该进行“Dispose”处理吗? - davidtbernal
1
回复:_"GDI+阻塞了很多进程。"_ 今天这个问题还存在吗?MSDN博客文章让我相信,在Windows 7中已经修复了这个问题(或者至少有所改善)。 - stakx - no longer contributing
@stakx - 我看到你提到的那行代码了;我不知道那个问题的答案。我们实际上只是在升级这个应用程序运行的服务器,从2003年开始;-)所以它还不适用于我!!尽管如此,我已经将应用程序重构为多进程运行,直到有另一个重大变化,我可能不会尝试将其放回到一个进程中。 - FastAl
@stakx:在Windows 10上,我使用调用Graphics.DrawImageParallel.ForEach获取单线程性能;现在使用WPF中的Media.DrawingContext.DrawImage,我获得了完全并行化的性能。我没有将任何东西渲染到屏幕上 - 只是裁剪、旋转并保存到磁盘上。也许问题特定于 Graphics.FromImage(Bitmap) - labreuer

5
如果您不介意采用WPF方法,可以尝试以下内容。以下是一个简单的重新缩放方法,接受图像流并生成包含结果JPEG数据的byte[]。由于您不想使用GDI+实际绘制图像,我认为这适合您,尽管它基于WPF。(唯一的要求是在项目中引用WindowsBase和PresentationCore。)
优点包括更快的编码(在我的机器上提高了200-300%)和更好的并行加速,尽管我在WPF渲染路径中也看到了一些不需要的序列化。请告诉我这对您有何作用。如果必要,我相信它还可以进一步优化。
代码:
 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();
    }
 }

5
我不确定为什么您执行Graphics.DrawImage似乎会序列化,但我实际上注意到了使用您一般的工作项排队模式存在竞争条件。竞争在于WaitOneSet之间。第一个工作项可能在任何其他工作项被排队之前就已经被Set。这将导致WaitOne在所有工作项完成之前立即返回。
解决方案是将主线程视为工作项。在排队开始之前增加pRuns,然后在排队完成后减少并发出等待信号处理程序,就像在正常工作项中一样。然而,更好的方法是如果可用的话,使用CountdownEvent类,因为它简化了代码。恰巧,我最近在另一个问题中发布了该模式

我无法使用CountdownEvent类。但是(如果主线程睡眠足够长的时间),工作函数中的if语句可能永远不会为真,对吗?你有什么解决这个问题的想法吗? - PTa
1
@PTa:不,你需要在循环之前递增pRuns,然后在循环结束之后再递减它......在调用WaitOne之前。所有的IncrementDecrement调用都应该平衡。最终会有人将其递减至0。这可能是一个工作项或主线程。你实际上已经做得大部分正确了。 - Brian Gideon
是的,很好的发现。@Pta:递减应该在Finally块中。 - H H

3

使用除GDI+之外的图像处理库。

我们在一个相当高流量的网站上使用ImageMagick来调整上传的图像大小(上传的图像通常为10-40 MPixels,但为了能够在网站中(在Flash模块中)使用它们,我们将它们调整为最小尺寸1500像素)。处理速度很快,效果非常好。

我们目前使用命令行界面启动新的ImageMagick进程。这会在启动新进程时产生一些开销,但由于图像非常大,通常只占总调整过程的一小部分时间片。也可以在进程内使用ImageMagick,但由于1.我们不需要它提供的额外性能,2.在其他进程中运行第三方软件感觉更好,因此还没有尝试过。

使用ImageMagick时,您还可以获得许多其他可能性,如更好的过滤和许多其他功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接