Paralelismo GDI + Redimensionamiento de Imagen .net

Intenté paralelizar el cambio de tamaño de jpegs usando .Net. Todos mis bashs fallaron, porque el Graphics.DrawImage-func parece bloquearse mientras está activo. Pruebe lo siguiente recortado:

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 

Todos los hilos esperan en graph.DrawImage (). ¿Hay alguna forma de acelerar el rendimiento del código utilizando otras funciones? ¿Es imposible usar Graphics.Draw con múltiples hilos? En la aplicación real, se deben cambiar el tamaño de varias imágenes al mismo tiempo (en una PC de cuatro núcleos), no siempre las mismas. El código publicado es solo para fines de prueba …

Gracias por adelantado

Editar: se actualizó el código de acuerdo a los comentarios

Usa procesos.

GDI + bloquea por proceso de muchas maneras. Sip, un dolor, pero no hay forma de evitarlo. Afortunadamente con tareas como esta (y cualquiera que procese archivos en el sistema de archivos), es muy fácil dividir la carga de trabajo entre múltiples procesos. Afortunadamente, parece que GDI + usa lockings, no mutex, ¡así que es concurrente!

Tenemos algunos progtwigs de gráficos donde trabajo para procesar imágenes. Un progtwigdor comienza 6-7 copias a la vez de un progtwig de conversión, en producción. Entonces no es desordenado, confía en mí. ¿Cortar? No te pagan para verte bonita. ¡Termina el trabajo!

Ejemplo barato (tenga en cuenta que esto NO funcionará en el ide, comstackrlo y ejecutar el 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 

No estoy seguro de por qué la ejecución de Graphics.DrawImage parece serializar para usted, pero en realidad noté una condición de carrera con su patrón general de poner en cola los elementos de trabajo. La carrera es entre WaitOne y el Set . Es posible que el primer elemento de trabajo se Set antes de que cualquiera de los otros haya sido aún cola. Eso hará que WaitOne regrese inmediatamente antes de que se hayan completado todos los elementos de trabajo.

La solución es tratar el hilo principal como si fuera un elemento de trabajo. Incremente pRuns una vez antes de que la cola comience y luego pRuns y pRuns el identificador de espera una vez que la cola esté completa tal como lo haría en un elemento de trabajo normal. Sin embargo, el mejor enfoque es utilizar la clase CountdownEvent si está disponible para usted, ya que simplifica el código. Por suerte, acabo de publicar el patrón en otra pregunta .

Si no te importa un enfoque WPF, aquí hay algo que probar. El siguiente es un método de reescalado simple que acepta flujos de imágenes y produce un byte [] que contiene los datos JPEG resultantes. Como no desea dibujar realmente las imágenes con GDI +, pensé que era adecuado para usted a pesar de estar basado en WPF. (El único requisito es hacer referencia a WindowsBase y PresentationCore en su proyecto).

Las ventajas incluyen una encoding más rápida (en un 200-300% en mi máquina) y una mejor aceleración paralela, aunque también veo cierta serialización no deseada en la ruta de representación de WPF. Déjame saber cómo funciona esto para ti. Estoy seguro de que podría optimizarse más si es necesario.

El código:

  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(); } } 

Use una biblioteca de procesamiento de imágenes que no sea GDI +.

Utilizamos ImageMagick en un sitio web de gran volumen que cambia el tamaño de las imágenes cargadas (las imágenes cargadas suelen ser de 10-40 MPixels pero para poder trabajar con ellas en el sitio web (en módulos Flash), las redimensionamos para que tengan el tamaño más pequeño 1500 píxeles). El procesamiento es bastante rápido y da excelentes resultados.

Actualmente comenzamos un nuevo proceso de ImageMagick usando la interfaz de línea de comando. Esto genera cierta sobrecarga al iniciar nuevos procesos, pero dado que las imágenes son tan grandes, suele ser una porción de tiempo bastante pequeña del proceso de cambio de tamaño total. También es posible utilizar ImageMagick en proceso, pero aún no lo hemos probado, ya que 1. no necesitamos el rendimiento adicional que brinda y 2. se siente bien ejecutar software de terceros en otros procesos.

Al usar ImageMagick, también obtiene una cantidad de otras posibilidades como un mejor filtrado y muchas otras funciones.