如何从负步幅的位图中复制像素数据?

本文关键字:复制 像素 像素数 数据 位图 | 更新日期: 2023-09-27 18:03:30

我正在寻找将位图转换为8bpp的最快方法。我找到了两种方法:

1。

        public static System.Drawing.Image ConvertTo8bpp(Bitmap oldbmp)
    {
        using (var ms = new MemoryStream())
        {
            oldbmp.Save(ms, ImageFormat.Gif);
            ms.Position = 0;
            return System.Drawing.Image.FromStream(ms);
        }
    }

2。 http://www.wischik.com/lu/programmer/1bpp.html

但: 1。导致非常低质量的结果(坏托盘)

2给了我一个负步幅的位图,当我试图锁定比特并将数据复制到字节数组时,我得到一个异常:试图读取或写入受保护的内存。这通常表明其他内存已损坏。

        BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, bmp.PixelFormat);
        this.stride = bmpData.Stride;
        this.bytesPerPixel = GetBytesPerPixel(bmp.PixelFormat);
        int length = bmpData.Stride * bmp.Height;
        if (this.stride < 0)
            this.data = new byte[-length];
        else
            this.data = new byte[length];
        Marshal.Copy(bmpData.Scan0, data, 0, length);
        //Unlock the bitmap
        bmp.UnlockBits(bmpData);

我如何使2给出一个积极的步伐?或者我如何使用负步幅的锁位复制数据??

如何从负步幅的位图中复制像素数据?

每次复制1行,计算每一行的起始指针为((byte*)scan0 + (y * stride))。对于正步幅和负步幅,代码是相同的。

这里的问题是Scan0指向第一个扫描行的开始,而不是数据的第一个字节的开始。在自底向上的位图中,第一个扫描线是位图数据末尾的Stride字节。

当您调用Marshal.CopyScan0复制数据时,它尝试从位置((Height-1)*Stride)开始复制(Height*Stride)字节。很明显,那将会消失在杂草中。

如果你只想复制位图数据,你必须用Scan0 - (Height-1)*Stride计算起始地址。这将使您从位图数据的开头开始。你可以把计算出的地址传递给Marshal.Copy

如果你想按顺序复制扫描行(即top, next, next,…)底部),然后你必须一次复制一行:从Scan0复制Stride字节,然后添加Stride(这是负的),复制该行,等等。Rick Brewster给出了正确的答案:https://stackoverflow.com/a/10360753/56778

我不知道为什么FromHbitmap方法创建的位图有些奇怪,但我知道你可以通过使用Bitmap bmpClone = (Bitmap)bmp.Clone();和在bmpClone上做LockBits来修复它。

另外,我发现如果您使用bmp.Clone(),则在完成克隆之前无法Dispose() bmp。

这也可以工作,让您尽早处理负步幅图像:

        Bitmap bmp = null;
        using (Bitmap bmpT = CopyToBpp(bmpO, 1))
        {
            bmp = new Bitmap(bmpT);
        }

来自c#文档BitmapData: stride是单行像素(扫描线)的宽度,四舍五入到四个字节的边界。如果步幅为正,则位图是自上而下的。如果步幅为负,则位图为自下而上

我设法解决了这个问题,而没有创建全新的位图对象,通过使用LockBits()ImageLockMode.UserInputBuffer。附上我想出来的代码,请随意使用。

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
public static class BitmapExtensions
{
    public static ProperBitmapData LockBitsProper(this Bitmap bitmap, ImageLockMode flags)
    {
        Rectangle bitmapBounds = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
        return bitmap.LockBitsProper(bitmapBounds, flags, bitmap.PixelFormat);
    }
    public static ProperBitmapData LockBitsProper(this Bitmap bitmap, Rectangle rect, ImageLockMode flags, PixelFormat format)
    {
        BitmapData bmpData = bitmap.LockBits(rect, flags, format);
        int byteCount;
        try
        {
            byteCount = Math.Abs(bmpData.Stride) * bmpData.Height;
            if (bmpData.Stride > 0) return new ProperBitmapData(bitmap, bmpData, byteCount, IntPtr.Zero);
        }
        catch
        {
            bitmap.UnlockBits(bmpData);
            throw;
        }
        // in case Stride is negative
        bitmap.UnlockBits(bmpData);
        // When Stride is negative, the LockBits locks the wrong memory area which results in AccessViolationException even when reading the right place
        // starting with Scan0 + (Height - 1) * Stride (also not properly documented).
        // This is a workaround to it using a user allocated area overload.
        // For some reason, in Windows Vista (SP0) Stride is (almost?) always negative, while in >=Windows 7 it is positive more often.
        // Some useful documentation: https://learn.microsoft.com/en-us/windows/win32/api/gdiplusheaders/nf-gdiplusheaders-bitmap-lockbits
        IntPtr userAllocatedArea = Marshal.AllocHGlobal(byteCount);
        try
        {
            // Actually when Stride is negative, Scan0 have to point to where the last row will be written.
            // This is not properly documented anywhere, and discovered just by trial and error.
            bmpData.Scan0 = (IntPtr)((long)userAllocatedArea - (bmpData.Height - 1) * bmpData.Stride);
            bmpData = bitmap.LockBits(rect, ImageLockMode.UserInputBuffer | flags, format, bmpData);
            try
            {
                return new ProperBitmapData(bitmap, bmpData, byteCount, userAllocatedArea);
            }
            catch
            {
                bitmap.UnlockBits(bmpData);
                throw;
            }
        }
        catch
        {
            Marshal.FreeHGlobal(userAllocatedArea);
            throw;
        }
    }
}
public class ProperBitmapData : IDisposable
{
    private Bitmap _bitmap;
    private BitmapData _bitmapData;
    private int _byteCount;
    private IntPtr _userAllocatedBuffer;
    public int Width => _bitmapData.Width;
    public int Height => _bitmapData.Height;
    public int Stride => _bitmapData.Stride;
    public PixelFormat PixelFormat => _bitmapData.PixelFormat;
    public IntPtr Scan0 => (_userAllocatedBuffer != IntPtr.Zero) ? _userAllocatedBuffer : _bitmapData.Scan0;
    public int Reserved => _bitmapData.Reserved;
    public int ByteCount => _byteCount;
    public ProperBitmapData(Bitmap bitmap, BitmapData bitmapData, int byteCount, IntPtr userAllocatedBuffer)
    {
        _bitmap = bitmap;
        _bitmapData = bitmapData;
        _byteCount = byteCount;
        _userAllocatedBuffer = userAllocatedBuffer;
    }
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing)
    {
        _bitmap?.UnlockBits(_bitmapData);
        _bitmap = null;
        _bitmapData = null;
        if (_userAllocatedBuffer != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(_userAllocatedBuffer);
            _userAllocatedBuffer = IntPtr.Zero;
        }
    }
    ~ProperBitmapData()
    {
        Dispose(false);
    }
}

用法示例:

using (ProperBitmapData bmpData = bitmap.LockBitsProper(ImageLockMode.ReadOnly))
{
    // Beware that bmpData.Scan0 here always points to the start of the allocated memory block.
    this.data = new byte[bmpData.ByteCount];
    Marshal.Copy(bmpData.Scan0, data, 0, bmpData.ByteCount);
}

我猜你得到的异常是由于

this.data = new byte[-length];

然后尝试将数据复制到负大小的字节数组中(我不知道这是如何编译的…)。