C#如何在picturebox中更新位图

13

我正在开发一个屏幕共享项目,不断地从Socket接收小块图像数据,并需要将它们更新到我已经拥有的某个初始桌面位图上。

基本上,我不断地从socket中读取数据(存储为jpeg图像),使用Image.FromStream()来获取这些图像,然后将接收到的块像素复制到完整的主位图上(在特定位置XY,我也从socket中获得)-这就是初始图像如何更新的。但之后我需要将其显示在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位图(对我来说似乎非常低效)之外,是否有一种方法可以在picturebox上直接绘制当前接收到的块本身。 这是我的绘图方法(使用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 bitmap 小块:

enter image description here 小块:

enter image description here

小块:

enter image description here

我每秒会获得大量的小块(30~40),有时它们的范围非常小(例如100X80像素的矩形),因此不必重新绘制整个位图...快速刷新全屏图像会影响性能...

希望我的说明清楚了。

期待回答。

谢谢。


2
将图像的像素格式更改为32bppPArgb,它的渲染速度比其他所有格式都快10倍。确保图像不必重新缩放以适应picturebox,ClientRectangle毫无帮助。永远不要使用Refresh,而是使用Invalidate(Rectangle)代替,其中矩形是实际必须重新绘制的图像部分。 - Hans Passant
你考虑过用性能分析器运行这段代码吗?通常它可以很容易地调整应用程序的性能。 - Mathias Lykkegaard Lorenzen
@Slashy,将大图片(1920X1080)分成小区域(许多PictureBoxes),并更新小区域,而不是整个大图片怎么样? - Artavazd Balayan
@Slashy 我认为它可能会有帮助。Visual Studio 中有一个内置的工具,你可以试试使用它。当构建性能要求高的应用程序时,分析器是一个非常常用的工具。 - Mathias Lykkegaard Lorenzen
我的建议是使用GDI进行实际绘制。另外,如果您已经在使用指针,则最好放弃IntPtr抽象而进入不安全模式。Pixel24*比IntPtr *= 3更清晰明了。 - hoodaticus
显示剩余9条评论
2个回答

3

不回答那个问题会让人感到遗憾。在我的测试中,以下方法在更新图片框的小部分时大约快了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);
    }
}

唯一有些棘手的部分是确定缩放后的矩形,特别是无效矩形,由于需要进行浮点数到整数的转换,因此我们确保它最终比所需稍微大一些,但不会更小。

(1) 抱歉,image 应该是 initial 变量。 (2) 不,这个重载只绘制图像的一部分 - imageRect。 - Ivan Stoev
1
没错。即使你试图在矩形框外绘制,它也会被裁剪掉,所以这就是我所说的“智能绘画”。 - Ivan Stoev
嘿!看起来在接下来的五年里我都会继续打扰你 :) 经过几个开发步骤后,我在我的项目中使用了不同的数据处理方法。 如之前所述,这是一个屏幕共享项目。简单地说:从服务器发送到客户端的每个数据包都包含需要应用于客户端源图像的更改区域(作为JPEG位图)。 你的解决方案对于以前的方法非常完美,其中我读取每个区域,将更改应用于源图像并进行绘制,但现在实际上每个数据包包含多个区域。 - Slashy
我认为逐个读取每个区域,锁定源位图的位,应用更改并重新绘制它(当然在解锁之前)可能非常低效。因此,我首先阅读所有区域,通过一次锁定/解锁位图来应用更改(每个整个接收包),然后使用invalidate,但这次我必须使整个屏幕无效,因为许多区域已更改,并且仅在整个缓冲区读取和整个更改区域应用于源图像时才进行无效化。希望我解释清楚了问题.... :) - Slashy
让我们在聊天中继续这个讨论。点击此处进入聊天室 - Slashy
显示剩余26条评论

0

如果你只需要在画布上进行绘制,那么你可以仅仅绘制一次初始图像,接着使用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方法是将图像绘制到图片框中,所以我才认为它被调用了两次。我不知道你所说的“损坏的像素”,在我的测试中它运行得很好。关于你的代码,需要注意的是,锁可能是最耗时的部分。 - Stefano d'Antonio
我明白你的意思:你是先绘制图像,然后再更改“SizeMode”吗?如果是这样,我不会期望它能够正常工作,因为“Graphics”只会一次性地复制图像,它不会保留你的更改,所以如果你调整了PB的大小,你将不得不从头开始重新绘制所有内容。你可能需要查看一些第三方控件,WinForms并不适合自定义图形。 - Stefano d'Antonio
那么我不会依赖于 SizeMode 进行绘制,我只会拉伸图像然后绘制它并将其保持为正常状态。但我仍然认为 WinForms PictureBox 不是这个问题的正确工具。 - Stefano d'Antonio

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