C#.NET中的Marshal.Copy方法会抛出AccessViolationException异常。

6

我正在开发一个C#应用程序,它可以显示来自相机的实时图像。

以下代码片段的问题是:当在线程中连续执行此函数时,我会在Marshal.Copy中遇到AccessViolationException。但是,当运行一次时(我得到一个静态图像),它可以成功运行。我猜测这可能与某些内存损坏问题有关。你有什么想法/建议来解决这个问题吗?

    private Image ByteArrayToImage(byte[] myByteArray) 
    {
        if (myByteArray != null)
        {
            MemoryStream ms = new MemoryStream(myByteArray);
            int Height = 504;
            int Width = 664;
            Bitmap bmp = new Bitmap(Width, Height, PixelFormat.Format24bppRgb);
            BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat);
            Marshal.Copy(myByteArray, 0, bmpData.Scan0, myByteArray.Length);
            bmp.UnlockBits(bmpData);

            return bmp;
        }
        return null;
    }

不确定,但您可以尝试在“LockBits”和“UnlockBits”周围放置锁定。因为此语句会锁定系统内存上的位图。而您的异常提示访问受保护的内存无效。 - Savaratkar
9个回答

10

在我看来,你总是试图将字节数组myByteArray.Length复制到位图缓冲区中。

你没有检查位图缓冲区是否确实足够大,所以很可能会写入位图缓冲区的末尾。

尝试检查myByteArray.Length是否大于bmpData.Stride x bmp.Height

如果是这种情况,你需要重新审视对宽度、高度和像素格式的硬编码值所做的假设。


6

不应一次性复制整个图像。位图对象的内存分配可能不是您所期望的。例如,第一行扫描可能会最后存储在内存中,这意味着第二行扫描的数据将超出位图对象的分配内存区域。此外,扫描线之间可能存在填充以将它们放置在偶地址上。

一次只复制一行,使用bmpData.Stride找到下一行扫描:

int offset = 0;
long ptr = bmpData.Scan0.ToInt64();
for (int i = 0; i < Height; i++) {
   Marshal.Copy(myByteArray, offset, new IntPtr(ptr), Width * 3);
   offset += Width * 3;
   ptr += bmpData.Stride;
}

也许我忽略了什么,但是我并没有看到您示例中每行指针的任何跳跃或不同之处。据我所见,此代码与 Marshal.Copy(myByteArray, 0, bmpData.Scan0, myByteArray.Length); 完全相同(长度将为 bmpData.Stride * bmp.Height)。 - Louis Somers
@LouisSomers 只有在图像从上到下存储在内存中的特殊情况下,才会执行相同的操作。 Stride 可能是负数。此外,在扫描线之间存在填充,但在最后一行扫描线之后可能没有填充。 - Guffa
bmpData.Stride 在每次迭代中都不会改变,它在循环期间是固定长度。它不知道您是向前还是向后循环,也不计算调用次数。为了支持分段存储,它必须是一个接受行索引的方法。文档还指出它“获取或设置位图对象的步幅宽度(也称为扫描宽度)”。它不会在每次调用时指向不同的位置。虽然您的代码可以正常工作,但单个对Marshal.Copy的调用应该更有效率。 - Louis Somers

2

我个人曾经在调试崩溃转储时看到过一些堆栈破坏问题,因为使用了.Length。

例如:

IntPtr ptr = bitmapdata.Scan0;
Marshal.Copy(pixeldata, 0, ptr, pixeldata.Length);

解决堆栈损坏的方法是以不同的方式计算.Length:
IntPtr ptr = bitmapdata.Scan0;
int bytes = Math.Abs(bitmapdata.Stride) * bmp.Height;
Marshal.Copy(pixeldata, 0, ptr, bytes);

bytes和.Length相差1个字节,导致堆栈损坏。

Math.Abs直接来自Microsoft的示例。因为对于自下而上的位图,Stride可能是负数。

Microsoft示例:https://msdn.microsoft.com/en-us/library/system.drawing.imaging.bitmapdata.scan0%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396#Examples

(+不要忘记.Unlock并将其添加到try-finally语句中。)


很好的观点。虽然我对Stride有所了解,但在数据块长度方面没有使用它。这解决了我的问题,而这个问题奇怪的是只出现在极少数情况下。 - Oak_3260548

0

请回答我:忘记了什么 ->

        // Unlock the bits right after Marshal.Copy
        bmp.UnlockBits(bmpData);

有人解决了这个问题吗?这已经是第四页没有答案了。使用 MSDN 上完全相同的代码:http://msdn.microsoft.com/en-us/library/system.drawing.imaging.bitmapdata.aspx
                        Bitmap bmp = new Bitmap("c:\\picture.jpg");

                        // Lock the bitmap's bits.  
                        Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
                        System.Drawing.Imaging.BitmapData bmpData =
                            bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
                            bmp.PixelFormat);

                        // Get the address of the first line.
                        IntPtr ptr = bmpData.Scan0;

                        // Declare an array to hold the bytes of the bitmap.
                        int bytes = bmpData.Stride * bmp.Height;
                        byte[] rgbValues = new byte[bytes];

                        // Copy the RGB values into the array.
                        //This causes read or write protected memory
                        System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);

在优化模式下运行exe而不是IDE中,这无法正常工作。有什么想法吗? 我尝试将其放入新项目中,并在按下按钮时附加到进程,如果多次按此按钮,则会出现错误,但在我的代码中,我只调用一次,无论如何都不确定为什么会出错。


你忽略了一个事实,即根据像素格式,一个像素可能超过一个字节,在int bytes = bmpData.Stride * bmp.Height;这行代码中。 - VVS

0

我进行了一些搜索,如果我们排除数组大小不正确的可能性,我们会得到BitmapData.Stride Property的备注

步幅是像素单行(扫描线)的宽度,向上舍入为四字节边界。如果步幅为正,则位图从上往下。 如果步幅为负,则位图从下往上。

因此,也许我们应该这样做:

BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat);
Marshal.Copy(myByteArray, 0, bmpData.Scan0 + 
( bmpData.Stride >= 0 ? 0 : bmpData.Stride*(bmp.Height-1) ),
myByteArray.Length);

但我想知道:我们已经创建了位图:new Bitmap(Width, Height, PixelFormat.Format24bppRgb); ……那么,它怎么可能是负数呢?
现在是ILSpy的时间了:

public Bitmap(int width, int height, PixelFormat format)
{
    IntPtr zero = IntPtr.Zero;
    int num = SafeNativeMethods.Gdip.GdipCreateBitmapFromScan0(width, height, 0, (int)format, NativeMethods.NullHandleRef, out zero);
    if (num != 0)
    {
            throw SafeNativeMethods.Gdip.StatusException(num);
    }
    base.SetNativeImage(zero);
}

// System.Drawing.SafeNativeMethods.Gdip
[DllImport("gdiplus.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
internal static extern int GdipCreateBitmapFromScan0(int width, int height, int stride, int format, HandleRef scan0, out IntPtr bitmap);

它将我指向了这里这里

Bitmap(
  [in]  INT width,
  [in]  INT height,
  [in]  INT stride,
  [in]  PixelFormat format,
  [in]  BYTE *scan0
);

stride [in]
类型:INT
整数,指定一个扫描线的开始和下一行之间的字节偏移量。通常这是像素格式中的字节数(例如每个像素16位,则为2)乘以位图宽度。传递给此参数的值必须是4的倍数。

传递0是什么意思? 不知道,找不到。有人知道吗?位图是否可以创建具有负步长?(通过.NET new Bitmap(Width, Height, PixelFormat.Format24bppRgb))。无论如何,我们至少需要检查BitmapData.Stride


0
我在BitmapSource Class的代码示例中找到了rawStride公式。尝试使用下面的代码创建一个数组,并尝试多次执行复制方法,看看是否会出现问题。如果可以,很可能是数组大小的问题。如果相机数据与内存中位图的大小不匹配,则可能需要逐行复制数据。
private byte[] CreateImageByteArray(int width, int height, PixelFormat pixelFormat)
{
    int rawStride = (width * pixelFormat.BitsPerPixel + 7) / 8;
    byte[] rawImage = new byte[rawStride * height];

    return rawImage;
}

另外一件你应该做的事情是确保在使用完Bitmap对象后正确地进行Dispose。我有时会看到一些通过非托管代码操作但没有清理的对象出现奇怪的结果。
此外,跨线程传递对象有时可能会很棘手。请参阅如何:对Windows窗体控件进行线程安全调用C#中使用Windows窗体控件进行线程安全调用文章。

0

可能

BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.WriteOnly, bmp.PixelFormat);

对于只写操作,ImageLockMode 参数无效 - 尝试使用 ReadWrite 或者 ReadOnly 参数? 也许这会有所帮助。


0

让我们尝试将ThreadApartmentState更改为单线程。

此外,请检查是否存在跨线程操作导致这些错误。


你好。这应该是一条评论而不是答案(我知道你还没有足够的声望来发布评论)。 - GHC

0

这种情况经常发生... bmpData.Scan0 指向位图的第一行, bmpData.Stride 是一行字节数。它大多数时候是一个负数。 这意味着位图的第二行在地址 bmpData.Scan0 - Math.abs(bmpData.Stride) 位图的第 n 行在地址 bmpData.Scan0 - n * Math.Abs(bmpData.Stride) 所以如果你从 bmpData.Scan0 复制超过一行,就会出现异常


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