JPEG 压缩导致 GDI+ 异常

JPEG Compression causes GDI+ Exception

所以,我目前正在开发一个 LAN-Video-Streaming 程序,它可以记录单个图像并将它们发送过来。因为每秒发送 30 张 1920x1080 图片太多了,要获得 30FPS,我做了一些研究并找到了 JPEG-Compression。问题是,当我尝试保存压缩的 JPEG 时,它会抛出一个 System.Runtime.InteropServices.ExternalException,以及附加信息:General error in GDI+.

这是我的代码:

Private Sub Stream() Handles StreamTimer.Tick

    If Streaming = True Then

        Try
            ScreenCap = New Bitmap(Bounds.Width, Bounds.Height)

            GFX = Graphics.FromImage(ScreenCap)
            GFX.CopyFromScreen(0, 0, 0, 0, ScreenCap.Size)

            Dim Frame As New Bitmap(ScreenCap, Resolution.Split(";")(0), Resolution.Split(";")(1))

            Dim jpgEncoder As ImageCodecInfo = GetEncoder(ImageFormat.Jpeg)
            Dim myEncoder As Encoder = Encoder.Quality
            Dim myEncoderParameters As New EncoderParameters(1)
            Dim myEncoderParameter As New EncoderParameter(myEncoder, Compression)
            myEncoderParameters.Param(0) = myEncoderParameter

            Frame.Save(My.Computer.FileSystem.SpecialDirectories.Temp & "\LSSFrame.jpg", jpgEncoder, myEncoderParameters) 'Error occurs in this line

            Using FS As New FileStream(My.Computer.FileSystem.SpecialDirectories.Temp & "\LSSFrame.jpg", FileMode.Open)
                Frame = Image.FromStream(FS)
                FrameSizeStatus.Text = Math.Round(FS.Length / 1000) & "KB"
                FS.Close()
            End Using

            PreviewBox.Image = Frame
            FPSStat += 1

            FlushMemory()

            If ViewerIPs.Count > 0 Then

                For i = 0 To ViewerIPs.Count - 1
                    SendFrame(ViewerIPs(i), Frame)
                Next

            End If
        Catch ex As Exception
            LostFrames += 1
        End Try
    End If

End Sub

感谢任何帮助!

在某种程度上,您没有处置您创建的 GraphicsBitmap 对象。错误信息不是很有帮助,但不处理这些信息会使资源无法恢复。

该过程中还有很多事情要做。如果它被分解成多个部分,可能更容易 fine-tune 性能等。

' form level objects
Private jEncParams As EncoderParameters
Private jpgEncoder As ImageCodecInfo
...
' inititalize somewhere when the process starts:
Dim quality As Int64 = 95

jpgEncoder = GetEncoder(ImageFormat.Jpeg)
Dim myjEnc As Imaging.Encoder = Imaging.Encoder.Quality

jEncParams = New EncoderParameters(1)
' quality is inverse to compression
jEncParams.Param(0) = New EncoderParameter(myjEnc, quality)

由于编码器和质量元素不会针对每个屏幕快照而改变,因此创建它们一次并重新使用它们。然后你的定时器事件:

Dim scrBytes = GetScreenSnap(1280, 720)
' do something to send them....maybe queue them?
Console.WriteLine("image size: {0}k", (scrBytes.Length / 1024).ToString)

优化 SendFrame 超出了本 Q/A 的范围,但获取屏幕截图与发送是分开的。

Private Function GetScreenSnap(w As Int32, h As Int32) As Byte()

    Using bmpScrn As New Bitmap(My.Computer.Screen.Bounds.Width, My.Computer.Screen.Bounds.Height)
        Using g As Graphics = Graphics.FromImage(bmpScrn)
            g.CopyFromScreen(0, 0, 0, 0, bmpScrn.Size)
        End Using       ' done with graphics

        Using bmpThumb As New Bitmap(bmpScrn, w, h),
            ms As New MemoryStream
            bmpThumb.Save(ms, jpgEncoder, jEncParams)
            Return ms.ToArray
        End Using        ' dispose of bmp
    End Using            ' dispose of bmpScrn

End Function

没有特别的原因,我正在缩略整个屏幕。你的似乎不使用 Bounds.Width, Bounds.Height 因为那会引用表格。如果表单最大化,它只能用作屏幕快照。要点:

  • 这是基于您 can/will 在流中发送字节数组的想法。因此,我将其保留为字节数组,而不是创建 BMP 只是为了(大概)稍后将其转换回来。
  • 不需要创建磁盘文件来获取大小。如果数组包含编码字节,它将是相同的大小。
  • 我从未使用过 COMPRESSION 参数,但我知道质量与压缩相反。

各种质量因素的结果尺寸:

100 = 462
95 = 254
90 = 195
80 = 147

严格来说您不需要编码器,bmpThumb.Save(ms, Imaging.ImageFormat.Jpeg) 也可以,但单个编码器对象提供更多微调。


对于发送部分,您可能需要 StackQueueLinkedList 来存储字节数组。这将进一步隔离获取图像与发送图像。该集合将是一种 ToDo/ToSend 列表。

然后,如果有多个收件人,我会考虑将 SendFrame 作为 Task,或许一次发送 2-3 个。接收者的数量可能会影响您获取新接收者的速度。