如何处理1位和4位图像?

时间:2018-06-27 21:53:01

标签: c# image-processing bitmapdata

BitmapLocker类用于快速读写Bitmap图像文件中的像素。

但是,Color GetPixel(int x, int y)void SetPixel(int x, int y, Color c)无法处理1位和4位图像。

public class BitmapLocker : IDisposable
{
    //private properties
    Bitmap _bitmap = null;
    BitmapData _bitmapData = null;
    private byte[] _imageData = null;

    //public properties
    public bool IsLocked { get; set; }
    public IntPtr IntegerPointer { get; private set; }
    public int Width 
    { 
        get 
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return _bitmapData.Width; 
        } 
    }
    public int Height 
    {
        get 
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return _bitmapData.Height; 
        } 
    }
    public int Stride 
    { 
        get
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return _bitmapData.Stride; 
        } 
    }
    public int ColorDepth 
    { 
        get 
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return Bitmap.GetPixelFormatSize(_bitmapData.PixelFormat); 
        } 
    }
    public int Channels 
    { 
        get
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked"); 
            return ColorDepth / 8; 
        } 
    }
    public int PaddingOffset 
    { 
        get 
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked"); 
            return _bitmapData.Stride - (_bitmapData.Width * Channels); 
        } 
    }
    public PixelFormat ImagePixelFormat 
    { 
        get
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return _bitmapData.PixelFormat; 
        } 
    }
    //public bool IsGrayscale 
    //{ 
    //    get 
    //    {
    //        if (IsLocked == false) throw new InvalidOperationException("not locked");
    //        return Grayscale.IsGrayscale(_bitmap); 
    //    } 
    //}

    //Constructor
    public BitmapLocker(Bitmap source)
    {
        IsLocked = false;
        IntegerPointer = IntPtr.Zero;
        this._bitmap = source;
    }

    /// Lock bitmap
    public void Lock()
    {
        if (IsLocked == false)
        {
            try
            {
                // Lock bitmap (so that no movement of data by .NET framework) and return bitmap data
                _bitmapData = _bitmap.LockBits(
                       new Rectangle(0, 0, _bitmap.Width, _bitmap.Height),
                       ImageLockMode.ReadWrite,
                       _bitmap.PixelFormat);

                // Create byte array to copy pixel values
                int noOfBytesNeededForStorage = Math.Abs(_bitmapData.Stride) * _bitmapData.Height;
                _imageData = new byte[noOfBytesNeededForStorage];

                IntegerPointer = _bitmapData.Scan0;

                // Copy data from IntegerPointer to _imageData
                Marshal.Copy(IntegerPointer, _imageData, 0, _imageData.Length);

                IsLocked = true;
            }
            catch (Exception)
            {
                throw;
            }
        }
        else
        {
            throw new Exception("Bitmap is already locked.");
        }
    }

    /// Unlock bitmap
    public void Unlock()
    {
        if (IsLocked == true)
        {
            try
            {
                // Copy data from _imageData to IntegerPointer
                Marshal.Copy(_imageData, 0, IntegerPointer, _imageData.Length);

                // Unlock bitmap data
                _bitmap.UnlockBits(_bitmapData);

                IsLocked = false;
            }
            catch (Exception)
            {
                throw;
            }
        }
        else
        {
            throw new Exception("Bitmap is not locked.");
        }
    }

    public Color GetPixel(int x, int y)
    {
        Color clr = Color.Empty;

        // Get color components count
        int cCount = ColorDepth / 8;

        // Get start index of the specified pixel
        int i = (Stride > 0 ? y : y - Height + 1) * Stride + x * cCount;

        int dataLength = _imageData.Length - cCount;

        if (i > dataLength)
        {
            throw new IndexOutOfRangeException();
        }

        if (ColorDepth == 32) // For 32 bpp get Red, Green, Blue and Alpha
        {
            byte b = _imageData[i];
            byte g = _imageData[i + 1];
            byte r = _imageData[i + 2];
            byte a = _imageData[i + 3]; // a
            clr = Color.FromArgb(a, r, g, b);
        }
        if (ColorDepth == 24) // For 24 bpp get Red, Green and Blue
        {
            byte b = _imageData[i];
            byte g = _imageData[i + 1];
            byte r = _imageData[i + 2];
            clr = Color.FromArgb(r, g, b);
        }
        if (ColorDepth == 1 || ColorDepth == 4 || ColorDepth == 8)
        // For 8 bpp get color value (Red, Green and Blue values are the same)
        {
            byte c = _imageData[i];
            clr = Color.FromArgb(c, c, c);
        }
        return clr;
    }

    public void SetPixel(int x, int y, Color color)
    {

        if (!IsLocked) throw new Exception();

        // Get color components count
        int cCount = ColorDepth / 8;

        // Get start index of the specified pixel
        int i = (Stride > 0 ? y : y - Height + 1) * Stride + x * cCount;

        try
        {
            if (ColorDepth == 32) // For 32 bpp set Red, Green, Blue and Alpha
            {
                _imageData[i] = color.B;
                _imageData[i + 1] = color.G;
                _imageData[i + 2] = color.R;
                _imageData[i + 3] = color.A;
            }
            if (ColorDepth == 24) // For 24 bpp set Red, Green and Blue
            {
                _imageData[i] = color.B;
                _imageData[i + 1] = color.G;
                _imageData[i + 2] = color.R;
            }
            if (ColorDepth == 1 || ColorDepth == 4 || ColorDepth == 8)
            // For 8 bpp set color value (Red, Green and Blue values are the same)
            {
                _imageData[i] = color.B;
            }
        }
        catch (Exception ex)
        {
            throw new Exception("(" + x + ", " + y + "), " + _imageData.Length + ", " + ex.Message + ", i=" + i);
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // free managed resources
            _bitmap = null;
            _bitmapData = null;
            _imageData = null;
            IntegerPointer = IntPtr.Zero;
        }
    }
}

例如,以下代码显示全黑输出:

public class MainClass
{
    public static void Main(string [] args)
    {
        Bitmap source = (Bitmap)Bitmap.FromFile(@"1_bit__parrot__monochrome.png");

        BitmapLocker locker = new BitmapLocker(source);
        locker.Lock();
        Bitmap dest = new Bitmap(source.Width, source.Height, locker.ImagePixelFormat);

        BitmapLocker locker2 = new BitmapLocker(dest);

        locker2.Lock();

        for (int h = 0; h < locker.Height; h++)            
        {
            for (int w = 0; w < locker.Width; w++)
            {
                locker2.SetPixel(w,h,locker.GetPixel(w,h));
            }
        }
        locker2.Unlock();
        locker.Unlock();

        dest.Palette = source.Palette; // copy color palette too!

        PictureDisplayForm f = new PictureDisplayForm(source, dest);
        f.ShowDialog();
    }
}

如何纠正此代码,使其可以处理1位和4位图像?

样本输入

1位单色 4位彩色
enter image description here enter image description here

2 个答案:

答案 0 :(得分:2)

对于小于8位的像素格式,将多个像素打包成一个字节。因此,对于8位,4位和1位格式,您不能拥有像这样的包罗万象的语句:

if (ColorDepth == 1 || ColorDepth == 4 || ColorDepth == 8)
{
    byte c = _imageData[i];
    clr = Color.FromArgb(c, c, c);
}

相反,基于像素格式,当检索像素数据时,必须计算字节中的位位置并从字节中提取适当的位-在这种情况下,该位将为“高”或“低”位4位图像或1位图像时为1位。相反,在设置像素数据时,只需更改字节中的某些位(基于像素格式)即可。

假设我们有一个4位格式的图像。图片数据可能看起来像这样:

bit index:     0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17  18  19  20  21  22  23
             +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
             | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 0 | 1 |
             +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
byte index:    0                               1                               2
pixel index:   0               1               2               3               4               5

此格式每个字节打包两个像素。因此,在检索像素数据时,我们首先计算像素的位索引:

int biti = (Stride > 0 ? y : y - Height + 1) * Stride * 8 + x * ColorDepth;

Stride是一行中的字节数,因此只需将其乘以高度* 8(对于一个字节中的8位),然后加上宽度* ColorDepth(对于每位的位数)像素)。

然后,我们需要弄清楚我们要检索字节的前四位还是后四位。为此,我们只需计算bitindex mod 8。显然,如果像素以字节开头,则为0(例如8 mod 8 = 0),否则为4。基于此,如果我们需要前四个位,则将字节移位四。 C#将前四位清零:

   +-----------------+                  
   |+---+---+---+---+|---+---+---+---+               +---+---+---+---+---+---+---+---+
   || 0 | 0 | 1 | 1 || 1 | 1 | 0 | 0 |      =>       | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
   |+---+---+---+---+|---+---+---+---+               +---+---+---+---+---+---+---+---+
   +-----------------+                     
             ===============>>

另一方面,如果我们需要最后四位,我们AND的图像数据字节的前四位被清零:

+---+---+---+---+---+---+---+---+  
| 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 |  
+---+---+---+---+---+---+---+---+  
              AND           
+---+---+---+---+---+---+---+---+  
| 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |  
+---+---+---+---+---+---+---+---+  
               =
+---+---+---+---+---+---+---+---+  
| 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 |  
+---+---+---+---+---+---+---+---+

在代码中,所有这些看起来像:

byte c = 0;
if (biti % 8 == 0)
{
     c = (byte)(_imageData[i] >> 4);
}
else
{
     c = (byte)(_imageData[i] & 0xF);
}

对于1位单色图像,我们希望获得单个位。为此,我们AND的图像数据字节的所有其他位都清零了(“掩码”)。例如,如果我们想获得索引5的位,则可以这样做:

+---+---+---+---+---+---+---+---+  
| 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 |  
+---+---+---+---+---+---+---+---+  
              AND           
+---+---+---+---+---+---+---+---+  
| 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |  
+---+---+---+---+---+---+---+---+  
               =
+---+---+---+---+---+---+---+---+  
| 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |  
+---+---+---+---+---+---+---+---+

如果结果为零,则我们知道该位为零,否则该位为“置位”。在代码中:

byte mask = (byte)(1 << bbi);
byte c = (byte)((_imageData[i] & mask) != 0 ? 1 : 0);

一旦我们检索了像素数据,我们就可以检索实际的颜色,因为GetPixel函数返回了一个Color对象。对于8位,4位和1位图像,像素数据实际上表示调色板的索引。调色板看起来像这样:

============= +-----+-----+-----++-----+-----+-----++-----+-----+-----+
              |  R  |  G  |  B  ||  R  |  G  |  B  ||  R  |  G  |  B  |  
    Color     +-----+-----+-----++-----+-----+-----++-----+-----+-----+
              | 000 | 016 | 005 || 020 | 120 | 053 || 117 | 002 | 209 |
============= +-----+-----+-----++-----+-----+-----++-----+-----+-----+
              |                 ||                 ||                 |
    Index     |        0        ||        1        ||        2        |
              |                 ||                 ||                 |
============= +-----------------++-----------------++-----------------+

我们可以访问调色板,因此可以检索颜色:

clr = Palette.Entries[c];

c是检索到的像素数据。

对于设置像素数据也做了类似的事情。有关C#中的位操作的大量信息,例如hereherehere

将所有内容放在一起,并与您现有的代码保持一致:

public class BitmapLocker : IDisposable
{
    //private properties
    Bitmap _bitmap = null;
    BitmapData _bitmapData = null;
    private byte[] _imageData = null;

    //public properties
    public bool IsLocked { get; set; }
    public IntPtr IntegerPointer { get; private set; }
    public int Width
    {
        get
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return _bitmapData.Width;
        }
    }
    public int Height
    {
        get
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return _bitmapData.Height;
        }
    }
    public int Stride
    {
        get
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return _bitmapData.Stride;
        }
    }
    public int ColorDepth
    {
        get
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return Bitmap.GetPixelFormatSize(_bitmapData.PixelFormat);
        }
    }
    public int Channels
    {
        get
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return ColorDepth / 8;
        }
    }
    public int PaddingOffset
    {
        get
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return _bitmapData.Stride - (_bitmapData.Width * Channels);
        }
    }
    public PixelFormat ImagePixelFormat
    {
        get
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return _bitmapData.PixelFormat;
        }
    }
    public ColorPalette Palette
    {
        get
        {
            if (IsLocked == false) throw new InvalidOperationException("not locked");
            return _bitmap.Palette;
        }
    }

    //Constructor
    public BitmapLocker(Bitmap source)
    {
        IsLocked = false;
        IntegerPointer = IntPtr.Zero;
        this._bitmap = source;
    }

    /// Lock bitmap
    public void Lock()
    {
        if (IsLocked == false)
        {
            try
            {
                // Lock bitmap (so that no movement of data by .NET framework) and return bitmap data
                _bitmapData = _bitmap.LockBits(
                       new Rectangle(0, 0, _bitmap.Width, _bitmap.Height),
                       ImageLockMode.ReadWrite,
                       _bitmap.PixelFormat);

                // Create byte array to copy pixel values
                int noOfBytesNeededForStorage = Math.Abs(_bitmapData.Stride) * _bitmapData.Height;
                _imageData = new byte[noOfBytesNeededForStorage];

                IntegerPointer = _bitmapData.Scan0;

                // Copy data from IntegerPointer to _imageData
                Marshal.Copy(IntegerPointer, _imageData, 0, _imageData.Length);

                IsLocked = true;
            }
            catch (Exception)
            {
                throw;
            }
        }
        else
        {
            throw new Exception("Bitmap is already locked.");
        }
    }

    /// Unlock bitmap
    public void Unlock()
    {
        if (IsLocked == true)
        {
            try
            {
                // Copy data from _imageData to IntegerPointer
                Marshal.Copy(_imageData, 0, IntegerPointer, _imageData.Length);

                // Unlock bitmap data
                _bitmap.UnlockBits(_bitmapData);

                IsLocked = false;
            }
            catch (Exception)
            {
                throw;
            }
        }
        else
        {
            throw new Exception("Bitmap is not locked.");
        }
    }

    public Color GetPixel(int x, int y)
    {
        Color clr = Color.Empty;

        // Get the bit index of the specified pixel
        int biti = (Stride > 0 ? y : y - Height + 1) * Stride * 8 + x * ColorDepth;
        // Get the byte index
        int i = biti / 8;

        // Get color components count
        int cCount = ColorDepth / 8;

        int dataLength = _imageData.Length - cCount;

        if (i > dataLength)
        {
            throw new IndexOutOfRangeException();
        }

        if (ColorDepth == 32) // For 32 bpp get Red, Green, Blue and Alpha
        {
            byte b = _imageData[i];
            byte g = _imageData[i + 1];
            byte r = _imageData[i + 2];
            byte a = _imageData[i + 3]; // a
            clr = Color.FromArgb(a, r, g, b);
        }
        if (ColorDepth == 24) // For 24 bpp get Red, Green and Blue
        {
            byte b = _imageData[i];
            byte g = _imageData[i + 1];
            byte r = _imageData[i + 2];
            clr = Color.FromArgb(r, g, b);
        }
        if (ColorDepth == 8)
        {
            byte c = _imageData[i];
            if(Palette.Entries.Length <= c)
                throw new InvalidOperationException("no palette");
            clr = Palette.Entries[c];
        }
        if (ColorDepth == 4)
        {
            byte c = 0;
            if (biti % 8 == 0)
            {
                c = (byte)(_imageData[i] >> 4);
            }
            else
            {
                c = (byte)(_imageData[i] & 0xF);
            }
            if (Palette.Entries.Length <= c)
                throw new InvalidOperationException("no palette");
            clr = Palette.Entries[c];
        }
        if (ColorDepth == 1)
        {
            int bbi = biti % 8;
            byte mask = (byte)(1 << bbi);
            byte c = (byte)((_imageData[i] & mask) != 0 ? 1 : 0);
            if (Palette.Entries.Length <= c)
                throw new InvalidOperationException("no palette");
            clr = Palette.Entries[c];
        }
        return clr;
    }

    public void SetPixel(int x, int y, Color color)
    {

        if (!IsLocked) throw new Exception();

        // Get the bit index of the specified pixel
        int biti = (Stride > 0 ? y : y - Height + 1) * Stride * 8 + x * ColorDepth;
        // Get the byte index
        int i = biti / 8;

        // Get color components count
        int cCount = ColorDepth / 8;

        try
        {
            if (ColorDepth == 32) // For 32 bpp set Red, Green, Blue and Alpha
            {
                _imageData[i] = color.B;
                _imageData[i + 1] = color.G;
                _imageData[i + 2] = color.R;
                _imageData[i + 3] = color.A;
            }
            if (ColorDepth == 24) // For 24 bpp set Red, Green and Blue
            {
                _imageData[i] = color.B;
                _imageData[i + 1] = color.G;
                _imageData[i + 2] = color.R;
            }
            if (ColorDepth == 8)
            {
                if (Palette.Entries.Length < 256)
                    throw new InvalidOperationException("no palette");
                byte index = 0;
                for (int j = 0; j < 256; j++)
                {
                    if(Palette.Entries[j].R == color.R && Palette.Entries[j].G == color.G && Palette.Entries[j].B == color.B)
                    {
                        index = (byte)j;
                        break;
                    }
                }
                _imageData[i] = index;
            }
            if (ColorDepth == 4)
            {
                if (Palette.Entries.Length < 16)
                    throw new InvalidOperationException("no palette");
                byte index = 0;
                for (int j = 0; j < 16; j++)
                {
                    if (Palette.Entries[j].R == color.R && Palette.Entries[j].G == color.G && Palette.Entries[j].B == color.B)
                    {
                        index = (byte)j;
                        break;
                    }
                }
                if (biti % 8 == 0)
                {
                    _imageData[i] = (byte)((_imageData[i] & 0xF) | (index << 4));
                }
                else
                {
                    _imageData[i] = (byte)((_imageData[i] & 0xF0) | index);
                }
            }
            if (ColorDepth == 1)
            {
                if (Palette.Entries.Length < 2)
                    throw new InvalidOperationException("no palette");
                byte index = 0;
                for (int j = 0; j < 2; j++)
                {
                    if (Palette.Entries[j].R == color.R && Palette.Entries[j].G == color.G && Palette.Entries[j].B == color.B)
                    {
                        index = (byte)j;
                        break;
                    }
                }
                int bbi = biti % 8;
                byte mask = (byte)(1 << bbi);
                if (index != 0)
                {
                    _imageData[i] |= mask;
                }
                else
                {
                    _imageData[i] &= (byte)~mask;
                }
            }
        }
        catch (Exception ex)
        {
            throw new Exception("(" + x + ", " + y + "), " + _imageData.Length + ", " + ex.Message + ", i=" + i);
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // free managed resources
            _bitmap = null;
            _bitmapData = null;
            _imageData = null;
            IntegerPointer = IntPtr.Zero;
        }
    }
}

注意:SetPixel中的for循环用于检索索引并不完全有效,因此,如果您经常使用该函数,则可能需要重新组织代码,以便采用索引值代替索引图像的颜色。

最后,为了使用此代码,我们必须在将更衣室对象用于索引图像之前复制调色板,这样看起来就像这样:

Bitmap source = (Bitmap)Bitmap.FromFile(@"testimage.png");

BitmapLocker locker = new BitmapLocker(source);
locker.Lock();
Bitmap dest = new Bitmap(source.Width, source.Height, locker.ImagePixelFormat);
if(source.Palette.Entries.Length > 0)
     dest.Palette = source.Palette;

BitmapLocker locker2 = new BitmapLocker(dest);

locker2.Lock();

for (int h = 0; h < locker.Height; h++)
{
     for (int w = 0; w < locker.Width; w++)
     {
          locker2.SetPixel(w, h, locker.GetPixel(w, h));
     }
}
locker2.Unlock();
locker.Unlock();

答案 1 :(得分:1)

1位和4位内容使用起来很烦人。因此,使用ConvertTo8BitConvertFrom8Bit函数,我处理的所有索引数据都将转换为每个像素更方便的1字节(8bpp)进行处理。

它们与GetImageData函数一起从图像中获取字节,并与BuildImage函数一起从字节中构建新的Bitmap

关于图像,通常要记住的重要一件事是,以像素为单位的线条宽度不一定与位数乘以宽度相同。首先,因为对于1bpp或4bpp,您总有多余的空间可以到达下一个完整字节,其次,because the .Net framework aligns image lines to multiples of 4 bytes。因此,在将图像作为字节处理时,务必保持一个“ stride”值,该值必须包含以字节为单位的实际数据宽度。

要记住的另一项重要事情是,您需要一个用于索引图像的调色板;它们的像素不是颜色,而是对调色板的引用。没有调色板,它们将无法显示任何内容,如果您忽略调色板,它们最终可能会使用Windows为每种像素格式提供的默认颜色,而这些颜色通常根本不是图像所需的。

最后,如果您在8位数组中编辑1位或4位数据,则​​必须确保切勿将超出原始像素格式允许的最大值的数据放入数组。因此,在4bpp数据中,永远不要有大于0x0F的字节,在1bpp中,字节中实际上应该只有0和1。

GetImageDataBuildImage函数:

/// <summary>
/// Gets the raw bytes from an image.
/// </summary>
/// <param name="sourceImage">The image to get the bytes from.</param>
/// <param name="stride">Stride of the retrieved image data.</param>
/// <returns>The raw bytes of the image.</returns>
public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride)
{
    if (sourceImage == null)
        throw new ArgumentNullException("sourceImage", "Source image is null!");
    BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
    stride = sourceData.Stride;
    Byte[] data = new Byte[stride * sourceImage.Height];
    Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
    sourceImage.UnlockBits(sourceData);
    return data;
}

/// <summary>
/// Creates a bitmap based on data, width, height, stride and pixel format.
/// </summary>
/// <param name="sourceData">Byte array of raw source data</param>
/// <param name="width">Width of the image</param>
/// <param name="height">Height of the image</param>
/// <param name="stride">Scanline length inside the data. If this is negative, the image is built from the bottom up (BMP format).</param>
/// <param name="pixelFormat">Pixel format</param>
/// <param name="palette">Color palette</param>
/// <param name="defaultColor">Default color to fill in on the palette if the given colors don't fully fill it.</param>
/// <returns>The new image</returns>
public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette, Color? defaultColor)
{
    Bitmap newImage = new Bitmap(width, height, pixelFormat);
    BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
    Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
    // Compensate for possible negative stride on BMP format.
    Boolean isFlipped = targetData.Stride < 0;
    Int32 targetStride = Math.Abs(targetData.Stride);
    Int64 scan0 = targetData.Scan0.ToInt64();
    for (Int32 y = 0; y < height; y++)
        Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
    newImage.UnlockBits(targetData);
    // Fix negative stride on BMP format.
    if (isFlipped)
        newImage.RotateFlip(RotateFlipType.Rotate180FlipX);
    // For indexed images, set the palette.
    if ((pixelFormat & PixelFormat.Indexed) != 0 && palette != null)
    {
        ColorPalette pal = newImage.Palette;
        for (Int32 i = 0; i < pal.Entries.Length; i++)
        {
            if (i < palette.Length)
                pal.Entries[i] = palette[i];
            else if (defaultColor.HasValue)
                pal.Entries[i] = defaultColor.Value;
            else
                break;
        }
        newImage.Palette = pal;
    }
    return newImage;
}

ConvertTo8BitConvertFrom8Bit函数:

/// <summary>
/// Converts given raw image data for a paletted image to 8-bit, so we have a simple one-byte-per-pixel format to work with.
/// </summary>
/// <param name="fileData">The file data.</param>
/// <param name="width">Width of the image.</param>
/// <param name="height">Height of the image.</param>
/// <param name="start">Start offset of the image data in the fileData parameter.</param>
/// <param name="bitsLength">Amount of bits used by one pixel.</param>
/// <param name="bigEndian">True if the bits in the original image data are stored as big-endian.</param>
/// <param name="stride">Stride used in the original image data. Will be adjusted to the new stride value.</param>
/// <returns>The image data in a 1-byte-per-pixel format, with a stride exactly the same as the width.</returns>
public static Byte[] ConvertTo8Bit(Byte[] fileData, Int32 width, Int32 height, Int32 start, Int32 bitsLength, Boolean bigEndian, ref Int32 stride)
{
    if (bitsLength != 1 && bitsLength != 2 && bitsLength != 4 && bitsLength != 8)
        throw new ArgumentOutOfRangeException("Cannot handle image data with " + bitsLength + "bits per pixel.");
    // Full array
    Byte[] data8bit = new Byte[width * height];
    // Amount of runs that end up on the same pixel
    Int32 parts = 8 / bitsLength;
    // Amount of bytes to read per width
    Int32 newStride = width;
    // Bit mask for reducing read and shifted data to actual bits length
    Int32 bitmask = (1 << bitsLength) - 1;
    Int32 size = stride * height;
    // File check, and getting actual data.
    if (start + size > fileData.Length)
        throw new IndexOutOfRangeException("Data exceeds array bounds!");
    // Actual conversion process.
    for (Int32 y = 0; y < height; y++)
    {
        for (Int32 x = 0; x < width; x++)
        {
            // This will hit the same byte multiple times
            Int32 indexXbit = start + y * stride + x / parts;
            // This will always get a new index
            Int32 index8bit = y * newStride + x;
            // Amount of bits to shift the data to get to the current pixel data
            Int32 shift = (x % parts) * bitsLength;
            // Reversed for big-endian
            if (bigEndian)
                shift = 8 - shift - bitsLength;
            // Get data and store it.
            data8bit[index8bit] = (Byte)((fileData[indexXbit] >> shift) & bitmask);
        }
    }
    stride = newStride;
    return data8bit;
}

/// <summary>
/// Converts given raw image data for a paletted 8-bit image to lower amount of bits per pixel.
/// </summary>
/// <param name="data8bit">The eight bit per pixel image data</param>
/// <param name="width">The width of the image</param>
/// <param name="height">The height of the image</param>
/// <param name="bitsLength">The new amount of bits per pixel</param>
/// <param name="bigEndian">True if the bits in the new image data are to be stored as big-endian.</param>
/// <param name="stride">Stride used in the original image data. Will be adjusted to the new stride value.</param>
/// <returns>The image data converted to the requested amount of bits per pixel.</returns>
public static Byte[] ConvertFrom8Bit(Byte[] data8bit, Int32 width, Int32 height, Int32 bitsLength, Boolean bigEndian, ref Int32 stride)
{
    Int32 parts = 8 / bitsLength;
    // Amount of bytes to write per width
    Int32 newStride = ((bitsLength * width) + 7) / 8;
    // Bit mask for reducing original data to actual bits maximum.
    // Should not be needed if data is correct, but eh.
    Int32 bitmask = (1 << bitsLength) - 1;
    Byte[] dataXbit = new Byte[newStride * height];
    // Actual conversion process.
    for (Int32 y = 0; y < height; y++)
    {
        for (Int32 x = 0; x < width; x++)
        {
            // This will hit the same byte multiple times
            Int32 indexXbit = y * newStride + x / parts;
            // This will always get a new index
            Int32 index8bit = y * stride + x;
            // Amount of bits to shift the data to get to the current pixel data
            Int32 shift = (x % parts) * bitsLength;
            // Reversed for big-endian
            if (bigEndian)
                shift = 8 - shift - bitsLength;
            // Get data, reduce to bit rate, shift it and store it.
            dataXbit[indexXbit] |= (Byte)((data8bit[index8bit] & bitmask) << shift);
        }
    }
    stride = newStride;
    return dataXbit;
}

请注意,bigEndian参数是指位块的顺序。通常,4bpp中的字节12 34只会为您提供像素1 2 3 4,在这种情况下,将采用大端规则(该值的数学上最大的部分将作为第一个像素处理)。 1bpp也是如此;值37通常会产生像素0 0 1 1 0 1 1 1。但是在我处理过的旧DOS游戏的某些自定义文件格式中,情况并非如此(4bpp 12 34会给出像素2 1 4 3),因此函数为什么要使用该参数。

类似地存在start参数,因为我使用的数据是从自定义文件格式读取的。通常,该值始终应该始终为0。