关于性能:C# 更新图片框中的位图

C# Update bitmap in picturebox

我正在做一个屏幕共享项目,我经常从 Socket 收到一小块图像,需要在我拥有的某个初始桌面位图上更新它们。

基本上我经常从套接字读取数据(数据存储为 jpeg 图像),使用 Image.FromStream() 检索图像并将接收到的块像素复制到完整的主位图(在特定位置 X 我也从套接字获得)- 这就是初始图像的更新方式。但是接下来是我需要在 Picturebox 上显示它的部分
我处理 Paint 事件并重新绘制它——整个初始图像非常大(在我的情况下为 1920X1080)。

这是我的代码:

    private void MainScreenThread()
    {
        ReadData();//reading data from socket.
        initial = bufferToJpeg();//first intial full screen image.
        pictureBox1.Paint += pictureBox1_Paint;//activating the paint event.
        while (true)
        {
            int pos = ReadData();
            x = BlockX();//where to draw :X
            y = BlockY();//where to draw :Y
            Bitmap block = bufferToJpeg();//constantly reciving blocks.
            Draw(block, new Point(x, y));//applying the changes-drawing the block on the big initial image.using native memcpy.

            this.Invoke(new Action(() =>
            {
                pictureBox1.Refresh();//updaing the picturebox for seeing results.
                // this.Text = ((pos / 1000).ToString() +"KB");
            }));
        }
    }

    private void pictureBox1_Paint(object sender, PaintEventArgs e)
    {
        lock (initial)
        {
            e.Graphics.DrawImage(initial, pictureBox1.ClientRectangle); //draws at picturebox's bounds
        }
    }

因为我的目标是高速性能(它是一种实时项目),我想知道是否没有任何方法可以在图片框上绘制当前收到的块本身而不是再次绘制整个 initial 位图 - 这对我来说似乎非常低效......
这是我的绘图方法(工作速度极快,使用 memcpy 复制块):

     private unsafe void Draw(Bitmap bmp2, Point point)
    {
        lock (initial)
        {  
            BitmapData bmData = initial.LockBits(new Rectangle(0, 0, initial.Width, initial.Height), System.Drawing.Imaging.ImageLockMode.WriteOnly, initial.PixelFormat);
            BitmapData bmData2 = bmp2.LockBits(new Rectangle(0, 0, bmp2.Width, bmp2.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp2.PixelFormat);
            IntPtr scan0 = bmData.Scan0;
            IntPtr scan02 = bmData2.Scan0;
            int stride = bmData.Stride;
            int stride2 = bmData2.Stride;
            int Width = bmp2.Width;
            int Height = bmp2.Height;
            int X = point.X;
            int Y = point.Y;

            scan0 = IntPtr.Add(scan0, stride * Y + X * 3);//setting the pointer to the requested line
            for (int y = 0; y < Height; y++)
            {
                memcpy(scan0, scan02 ,(UIntPtr)(Width * 3));//copy one line

                scan02 = IntPtr.Add(scan02, stride2);//advance pointers
                scan0 = IntPtr.Add(scan0, stride);//advance pointers//
            }


            initial.UnlockBits(bmData);
            bmp2.UnlockBits(bmData2);
        }
    }

这里有一些完整的主位图示例,以及我正在获取并需要在完整的位图上绘制的其他小块。

完整位图:

full

小块:

enter

小块:

enter

小方块:

enter

我每秒会得到大量的小块(30~40),有时它们的边界非常小(例如 100X80 像素的矩形),因此不需要再次重绘整个位图......快速刷新一个完整的屏幕图像会影响性能...

我希望我的解释很清楚。

期待答案。

谢谢。

相关讨论
  • 我认为您可以使用 Control.CreateGraphics 从图片框中获取图形,然后使用 Graphics.DrawImage(image, x, y) 在其顶部进行绘制,这样您就可以避免重绘初始图像。但我不知道它的效率如何,无论如何它可能会在引擎盖下对其进行处理。可能值得做一些性能测试。
  • 将图像的像素格式更改为 32bppPArgb,它的渲染速度比其他所有的快 10 倍。确保图像永远不必重新缩放以适应图片框,ClientRectangle 根本没有帮助。永远不要使用 Refresh,使用 Invalidate(Rectangle) 代替矩形是实际必须重绘的图像部分。
  • @Hans Passant 我得到的图像是 jpeg。 ..(24bpprgb)..我必须显示缩放的图像..它是为了屏幕共享的目的..所以如果有人有 1920X1080 分辨率,并且他将屏幕共享给具有 1600X880 分辨率的人客户端将无法看到整个屏幕...如果图像更大,那么他的计算机分辨率将无法看到整个图像;)所以我认为必须进行拉伸这里。用于正确显示屏幕。
  • @Hans Passant 关于您的其余答案,我明天会尝试回复。是时候睡觉了:)
  • @Slashy您需要清除画布还是只需要在上面绘制?
  • @Unit 仅在顶部绘制。
  • 您是否考虑过通过分析器运行此代码?它通常可以很容易地调整应用程序的性能。
  • @Slashy,如何将大图片(1920X1080)划分为小区域(许多图片框)并更新小区域,而不是大图片?
  • @Slashy,您是否也尝试过使用 Rectangle(更新区域)而不是 .Refresh() 的 Invalidate()?
  • @MathiasLykkegaardLorenzen 我以前没有使用过探查器,但它是否证明了 queryperformancecounterStopWatch 类不提供的其他任何东西?(对不起这个问题,我以前从未使用过)
  • @ArtavazdBalayan 不现实..我不能做那样的事情哈哈。它必须是动态图片框(在调整大小的上下文中)。关于无效-我会尝试它..不知道这个方法有一个重载..
  • @Slashy我实际上认为它可能。 Visual Studio 中内置了一个。尝试使用它。在构建性能要求高的应用程序时,分析器是一种非常常用的工具。
  • 我的建议是使用 GDI 进行实际绘画。此外,在我看来,如果您已经在使用指针,那么您不妨进入 unsafe 而不是通过 IntPtr 抽象工作。 Pixel24* 比 IntPtr *= 3 清晰得多。
  • @hoodaticus GDI 这意味着?bitblt?我正在使用 memcpy 仅一个原因:它的工作方式比使用然后直接使用指针要快..不要问我为什么..memcpy 工作得非常快.. ..根据我的基准测试,速度提高了 7-7.5 倍... 🙂

如果没有答案就留下这个问题是很遗憾的。在我的测试中更新图片框的一小部分时,以下速度大约快 10 倍。它所做的基本上是智能无效(仅使位图的更新部分无效,考虑缩放)和智能绘画(仅绘制图片框的无效部分,取自 e.ClipRectangle 并考虑缩放):

private Rectangle GetViewRect() { return pictureBox1.ClientRectangle; }

private void MainScreenThread()
{
    ReadData();//reading data from socket.
    initial = bufferToJpeg();//first intial full screen image.
    pictureBox1.Paint += pictureBox1_Paint;//activating the paint event.
    // The update action
    Action<Rectangle> updateAction = imageRect =>
    {
        var viewRect = GetViewRect();
        var scaleX = (float)viewRect.Width / initial.Width;
        var scaleY = (float)viewRect.Height / initial.Height;
        // Make sure the target rectangle includes the new block
        var targetRect = Rectangle.FromLTRB(
            (int)Math.Truncate(imageRect.X * scaleX),
            (int)Math.Truncate(imageRect.Y * scaleY),
            (int)Math.Ceiling(imageRect.Right * scaleX),
            (int)Math.Ceiling(imageRect.Bottom * scaleY));
        pictureBox1.Invalidate(targetRect);
        pictureBox1.Update();
    };

    while (true)
    {
        int pos = ReadData();
        x = BlockX();//where to draw :X
        y = BlockY();//where to draw :Y
        Bitmap block = bufferToJpeg();//constantly reciving blocks.
        Draw(block, new Point(x, y));//applying the changes-drawing the block on the big initial image.using native memcpy.

        // Invoke the update action, passing the updated block rectangle
        this.Invoke(updateAction, new Rectangle(x, y, block.Width, block.Height));
    }
}

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    lock (initial)
    {
        var viewRect = GetViewRect();
        var scaleX = (float)initial.Width / viewRect.Width;
        var scaleY = (float)initial.Height / viewRect.Height;
        var targetRect = e.ClipRectangle;
        var imageRect = new RectangleF(targetRect.X * scaleX, targetRect.Y * scaleY, targetRect.Width * scaleX, targetRect.Height * scaleY);
        e.Graphics.DrawImage(initial, targetRect, imageRect, GraphicsUnit.Pixel);
    }
}

唯一棘手的部分是确定缩放的矩形,尤其是用于无效的矩形,因为需要进行浮点到 int 的转换,所以我们确保它最终比需要的大一点,但不能少。

相关讨论
  • 我看到有很多损坏的像素..等等..我应该在您对 initialblock 的回答中替换 image 变量?
  • 我不明白这一点..您仍在重绘完整的 initial 图像...这里 e.Graphics.DrawImage(initial, targetRect, imageRect, GraphicsUnit.Pixel);
  • (1) 抱歉,image 应该是 initial 变量。 (2) 不,这个重载只绘制图像的一部分——imageRect。
  • 这是我的测试的一个小提琴。将其复制/粘贴到一个新的 WinForms 项目,然后使用 testRect 大小或 pb_Paint (注释所有内容并取消注释最后一行以查看部分与完整图像绘制的区别)。
  • 惊人的!它的工作速度提高了 2 到 3 倍!只是我想了解的几件事:1)你能简要解释一下 invlidate 方法吗?它是在重绘一个区域吗?我不确定 🙂 2) 你只是在绘画事件中计算了缩放因子,对吗?3) 不是很有必要,但如果你对此有一些微优化,我想看看。毕竟我想要最好的表现:) 非常感谢。
  • (1) Invalidate 在 MSDN 中有很好的解释:使控件的指定区域失效(将其添加到控件的更新区域,也就是下次绘制操作时将重新绘制的区域),并导致绘制消息发送到控制。然后在备注中:调用Invalidate方法不会强制同步绘制;要强制同步绘制,请在调用 Invalidate 方法后调用 Update 方法。
  • (2) 是的,我做到了。还有更新操作中的逆比例因子。 (3) 不,我没有,在发布答案之前我所做的只是玩弄你在小提琴中看到的代码。
  • 好吧!以及如何在绘画事件中获得控制更新区域?那是 e.ClipRectangle 部分线吗?
  • 确切地。即使你试图在那个矩形之外绘制,它还是会被剪掉,所以这就是我所说的智能绘画。
  • DrawImage 是如何实现的,这让我很感兴趣......微软的 Graphics 类......我想知道我是否可以更快地实现它
  • 您看不到实际的实现,因为 DrawImage 是相应 Gdi 方法的简单package器。而且 Gdi 是用非托管 C 编写的,我认为没有可用的源代码。
  • 好吧!非常感谢你,你不知道这有多大帮助:)
  • 不客气:) 如果我找到更快的东西,我会告诉你的。快乐编码!
  • 我想听听:) 再次感谢!
  • 正在环顾四周...是否可以使用本机 bitblt 方法来实现它?向下滚动到 OnPaint 事件 - 他们使用此方法绘制而不是 DrawImage() pinvoke.net/default.aspx/gdi32 /BitBlt.html
  • 我正在考虑这样的事情,但看起来它不支持缩放。如果设置源和/或目标转换,我不记得它是否会起作用。
  • 我一直在玩无效和更新代码..似乎整个位图仍然有更新,尽管我仅将 Invalidate(Rectangle) 用于特定区域..我只是在位图上方绘制了它的完整边界,我希望只看到 testRect 区域变为绿色,其余区域仍保持蓝色(尽管我绘制了整个位图),因为在 OnPaint 事件中只有特定区域得到更新!所以在视觉上我应该只看到红色矩形变成绿色,其余的蓝色区域应该保留。 dotnetfiddle.net/pssVe9
  • 也许我做错了什么?很抱歉有很多问题..我真的很感谢你的帮助:)。注意我没有使用缩放方法,因为我只是不需要这个测试。
  • 您需要知道的是,Paint 事件中的 e.ClipRectangle 并不总是您用于 Invalidate 调用的那个。例如,第一次绘制图片框时,它将包含整个客户矩形。此外,如果您调整表单/图片框的大小,那么它可能会有所不同。这基本上就是 Windows 的工作方式。对您来说重要的是尽量减少位图更新所强制的重绘区域。
  • 嘿!很长一段时间,但我仍在为此寻找一些小的改进,如果我错了,请纠正我,但是在此处调用 Invalidate() 方法 pictureBox1.Invalidate(targetRect); 之后,pictureBox1.Update(); 行是不必要的,因为 Invalidate() 方法也是导致向控件发送绘制消息,这意味着它将自动触发绘制事件。我对吗?msdn.microsoft.com/en-us/library/598t492a(v=vs.110).as??px @Ivan Stoev
  • 这是正确的。我放它是为了测试包括 DrawBitmap 在内的整个操作的速度。没有技术需要强制立即应用 Invalidated 区域,因此您可以删除 Update 调用并让它在 Windows 决定时重新绘制。
  • 嘿!看来我在接下来的 5 年内不会停止打扰你 🙂 经过几个步骤的开发,我在我的项目中使用不同的方法进行数据处理。如前所述,这是一个屏幕共享项目。一个简短的解释:从服务器发送到客户端的每个包都包含需要在客户端的源图像上应用的更改区域(作为 jpeg 位图)。您的解决方案非常适合以前的方法,我在其中读取每个区域,在源图像上应用更改并绘制它,但现在实际上每个包都包含几个区域
  • 大声笑:) 新年快乐!
  • 老实说,我认为阅读每个区域,锁定源位图的位,应用更改并重新绘制它(当然不是在解锁之前)可能效率很低。所以我首先读取所有区域,通过锁定/解锁位图一次(每个收到的整个包)应用更改,然后使用无效但这次我必须使*整个屏幕无效,因为许多区域发生了变化,并且仅当整个缓冲区读取和整个更改区域应用于源图像时才进行失效。我希望我能很好地解释这个问题.... 🙂
  • 还有一种方法只能使更改的区域无效吗?(但只有一次!在整个区域应用之后。例如,对于一个包含 4 个更改的矩形的包,我之前所做的是:1.read area-->lockBits() - -> 应用更改 --> unlockBits() --> invalidate(area)( 导致图片框立即重新绘制). 现在的方法是 1.for(int i=0;i<4;i ) 读取区域,应用更改(当我说应用时,我的意思是只复制它的像素(使用不安全的指针)。2.然后才需要重绘,但这一次,整个屏幕。我知道它很长,但我希望你有疑问.. .
  • 新年快乐 :):)
  • 现在谈这个话题。不幸的是,这种优化不起作用,因为剪辑矩形将是无效矩形的联合。我想不出其他优化。图形有所谓的 Clip 区域,但我不知道如何在矩形上分解它。我看到的唯一方法是在应用更改后,对每个更改的矩形执行 Invalidate ,然后立即执行 Update
  • 哦...我理解..但是触发每个区域的绘制.. 也可能更慢..我想我会在应用更改后使每个区域无效..最后我将使用'强制绘制事件更新()'......(甚至认为这将是一个联合并且包含一些额外的像素......我猜我将不得不测量这两种方法。)顺便说一下......我从来没有真正理解过的东西,当我使用 Invalidate() 它不会立即触发绘画事件,对吗?那么它什么时候绘制它?(如果以后没有 Update() 调用)。非常感谢。我真的很感谢你的帮助
  • 当您执行 Invalidate 时,相应的矩形被添加到无效的窗口区域,并且 WM_PAINT 消息被发送到窗口。稍后它由触发 Paint 事件的 WndProc 处理。诀窍是绘制消息始终保留在消息队列的末尾,因此它们在所有其他消息之后处理,从而将重绘保持在最低限度。因此,如果有数百个无效事件,通常它们会导致一次绘制。
  • 让我们在聊天中继续这个讨论。
  • 是的..但是当你使用 Update() 时......你强制重新绘制......所以我认为我应该在整个 Invalidate() 调用之后使用一个 Update() ..(在读取并处理缓冲区中的整个区域之后)...尽管有人建议我..查看 WPF 平台..因为 winForms 并不打算极速绘图..尽管它现在工作正常 🙂 @Ivan Stoev

如果你只需要在画布上绘制,你可以只绘制一次初始图像,然后使用 CreateGraphics()DrawImage 更新内容:

ReadData();
initial = bufferToJpeg();
pictureBox1.Image = initial;
var graphics = pictureBox1.CreateGraphics();
while (true)
{
    int pos = ReadData();
    Bitmap block = bufferToJpeg();
    graphics.DrawImage(block, BlockX(), BlockY());
}

我将通过性能比较来更新答案,因为我不相信这会带来任何重大好处;不过,它至少会避免双 DrawImage。

相关讨论
  • @Slashy:我假设 Draw 方法正在将图像绘制到图片框,这就是为什么我认为它被调用了两次。我不确定您所说的"损坏的像素"是什么意思,它在我的测试中运行良好。关于您的代码的注释是锁可能是最耗时的部分。
  • 我明白您的意思:您正在绘制图像,然后更改 SizeMode?如果是这样,我不希望它仅作为 Graphics blit 图像工作,它不会保留您的更改,因此如果您调整 PB 的大小,则必须从头开始重新绘制所有内容。您可能想看看一些第三方控件,WinForms 不适合自定义图形。
  • 然后我不会依赖 SizeMode 进行绘图,我只会拉伸图像然后绘制它并让它保持正常。但我仍然认为 WinForms PictureBox 不适合这个。

以上是关于性能:C# 更新图片框中的位图的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>