如何将原始数据显示为图像(Visual Studio c#)

14

我将会收到一些原始数据并储存在字节数组中,每2个字节是一个像素值(16 bits/px)。首先,该数组将包含100x100*2个字节(足够存储100x100像素图像)。我想在窗口中显示这些数据。最终,我想用新数据刷新图像,让它看起来像一个视频流。不需要严格的帧率。如何完成这个任务?是否有C#的代码示例?

编辑: 在一些建议和阅读了数十个类似问题的回答之后,我仍然无法解决问题。这是我尝试做的事情的一般想法,但图片没有显示在表格上。我的实现有什么具体问题以及如何解决?

// array of data I collected
byte[] dataArray = new byte[100 * 100 * 2]; 
//create a pointer to the data
IntPtr hglobal = Marshal.AllocHGlobal(100 * 100 * 2);

// copy my array to global
Marshal.Copy(dataArray, 0, hglobal, dataArray.Length);
// create a bitmap: 100x100 pixels, 2bytes/pixel, 16bitgrayscale
Bitmap newBitmap = new Bitmap(100, 100, 2 * 100, PixelFormat.Format16bppGrayScale, hglobal);

// display bitmap
pictureBox1.Image = newBitmap;

// free the memory
Marshal.FreeHGlobal(hglobal);

你可能想看一下这两个链接:https://stackoverflow.com/questions/16622408/set-the-pixels-of-image-to-the-values-stored-in-an-array-of-integers 和 https://dev59.com/nnHYa4cB1Zd3GeqPIzUL - mcy
你能够遍历数组并使用 Bitmap.SetPixel() 吗? - MahanGM
正如@Eldarien所说,您需要使用Format48bppRgb - Jony Adamit
2个回答

9
主要问题是PixelFormat.Format16bppGrayScale不被支持(至少在我的Win 8.1 x64系统上)。因此,在显示之前,您必须将图像转换为RGB:
private void Form1_Load(object sender, EventArgs e)
{
    //Create pixel data to put in image, use 2 since it is 16bpp
    Random r = new Random();
    int width = 100;
    int height = 100;
    byte[] pixelValues = new byte[width * height * 2];
    for (int i = 0; i < pixelValues.Length; ++i)
    {
        // Just creating random pixel values for test
        pixelValues[i] = (byte)r.Next(0, 256);
    }

    var rgbData = Convert16BitGrayScaleToRgb48(pixelValues, width, height);
    var bmp = CreateBitmapFromBytes(rgbData, width, height);

    // display bitmap
    pictureBox1.Image = bmp;
}

private static byte[] Convert16BitGrayScaleToRgb48(byte[] inBuffer, int width, int height)
{
    int inBytesPerPixel = 2;
    int outBytesPerPixel = 6;

    byte[] outBuffer = new byte[width * height * outBytesPerPixel];
    int inStride = width * inBytesPerPixel;
    int outStride = width * outBytesPerPixel;

    // Step through the image by row
    for (int y = 0; y < height; y++)
    {
        // Step through the image by column
        for (int x = 0; x < width; x++)
        {
            // Get inbuffer index and outbuffer index
            int inIndex = (y * inStride) + (x * inBytesPerPixel);
            int outIndex = (y * outStride) + (x * outBytesPerPixel);

            byte hibyte = inBuffer[inIndex + 1];
            byte lobyte = inBuffer[inIndex];

            //R
            outBuffer[outIndex] = lobyte;
            outBuffer[outIndex + 1] = hibyte;

            //G
            outBuffer[outIndex + 2] = lobyte;
            outBuffer[outIndex + 3] = hibyte;

            //B
            outBuffer[outIndex + 4] = lobyte;
            outBuffer[outIndex + 5] = hibyte;
        }
    }
    return outBuffer;
}

private static Bitmap CreateBitmapFromBytes(byte[] pixelValues, int width, int height)
{
    //Create an image that will hold the image data
    Bitmap bmp = new Bitmap(width, height, PixelFormat.Format48bppRgb);

    //Get a reference to the images pixel data
    Rectangle dimension = new Rectangle(0, 0, bmp.Width, bmp.Height);
    BitmapData picData = bmp.LockBits(dimension, ImageLockMode.ReadWrite, bmp.PixelFormat);
    IntPtr pixelStartAddress = picData.Scan0;

    //Copy the pixel data into the bitmap structure
    System.Runtime.InteropServices.Marshal.Copy(pixelValues, 0, pixelStartAddress, pixelValues.Length);

    bmp.UnlockBits(picData);
    return bmp;
}

这个想法来自于这个讨论串


将字节转换为48bpp的速度已经达到了极限,创建位图也无法更快。因此,您需要尝试每秒传输10张图像,并查看其效果。我会更改的部分是最终绘图的位置(图像控件)-在窗体的OnPaint事件上自己绘制位图,并使用双缓冲应该有所帮助。 - Eldarien
请问您能否解释一下“在窗体的OnPaint事件中自己绘制位图”的意思?我目前在窗体上有一个pictureBox组件。难道不应该使用它来显示图像吗? - Nazar
所以,我移除了pictureBox并在我的窗体上放置了一个panel。但是需要将此面板作为类吗?如何调用_paint_事件? - Nazar
是的,您必须将其制作为类才能覆盖其OnPaint事件。要强制重新绘制,可以使用Refresh方法:https://msdn.microsoft.com/zh-cn/library/system.windows.forms.control.refresh(v=vs.110).aspx - Eldarien
对于你的具体情况,可以进行很多优化。但那是另外一个故事了,所以如果你需要帮助,请打开另一个问题,并详细说明你的需求,我们将乐意提供帮助。对于这个问题,我认为你应该接受Eldarien的答案。 - Ivan Stoev
显示剩余5条评论

5
使用此 Bitmap 构造函数:
public Bitmap(
    int width,
    int height,
    int stride,
    PixelFormat format,
    IntPtr scan0
)

你需要传递位图的形状、步幅(每行多少字节,包括填充)、像素格式和像素数据作为 void * 指针。你可以使用 Marshal.AllocHGlobal 创建后者,并使用指针操作正常填充它。创建位图后,请不要忘记释放此内存。 根据更新后的问题进行编辑: 只需调用 IntPtr.ToPointer() 即可获得指针。如果您熟悉 C 语言,则其余部分应该很容易理解:
var p=(char *)hglobal.ToPointer();  // bad name by the way, it's not a handle, it's a pointer
p[0]=0;                             // access it like any normal pointer

然而,在C#中,直接操作内存通常不被鼓励,但你可以使用Marshaller从托管到非托管复制内存:

Marshal.Copy(dataArray, 0, hglobal, dataArray.Length); // again, terrible name

Bitmap是一个Image(也就是说,它派生自它),但是你使用Graphics.DrawImage()方法是错误的。正如错误所说,它不是一个静态方法,你需要将它绘制到特定的图形上下文中。现在,这个图形上下文是由你决定的:

  • 如果你想在响应WM_PAINT时绘制它,请使用Paint事件--它会为你提供一个特殊的Graphics对象,按照窗口系统的指示设置了剪切等。
  • 如果你想将它绘制到位图上,以便稍后显示(常见用法,也称为双缓冲),请在源位图上使用Graphics.FromImage(),然后在其上绘制位图。

你可以(也应该)在从Bitmap构造函数中获取结果后立即删除虚拟内存缓冲区。不要泄露内存,使用try..finally结构。


谢谢。您能否提供一些关于如何使用这个位图的更多信息?该怎么做?如何显示图像?我对此非常陌生。也许您有一些示例参考资料? - Nazar
你可以从图像或框架的“Paint”事件中获取一个“Graphics”上下文,并使用“Graphics.DrawImage”绘制你的图像。 - Blindy
1
已由答案进行更新,希望能有所帮助。不过,你真的应该阅读一篇关于GDI/GDI+的深入文章,因为大多数初学者都会觉得他们无法完全控制用户的屏幕这一事实非常困惑。 - Blindy
好的,谢谢。我会继续努力。关于“图形上下文” - 我只想在窗口应用程序表单中显示图像(在PictureBox中?)或在单独的表单上显示。是否可以在我想要的时候显示图像,而不是作为事件响应? - Nazar
那么,正确的方法是什么?我希望图像序列在表单上不断更新,类似于视频。 - Nazar
显示剩余2条评论

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