Как я могу работать с 1-битными и 4-битными изображениями? - PullRequest
0 голосов
/ 28 июня 2018

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 голосов
/ 01 июля 2018

1-битный и 4-битный контент довольно неудобен для работы. По этой причине любые индексированные данные, с которыми я работаю, просто конвертируются в более удобный 1 байт на пиксель (8 бит / с) для обработки с использованием функций ConvertTo8Bit и ConvertFrom8Bit.

Они работают в тандеме с функцией GetImageData для получения байтов из изображения и функцией BuildImage для создания нового Bitmap из байтов.

Одна важная вещь, которую следует помнить об изображениях в целом, это то, что ширина строки в пикселях не обязательно совпадает с количеством битов, умноженных на ширину. Во-первых, потому что для 1bpp или 4bpp у вас все равно может быть избыток для перехода к следующему полному байту, а во-вторых, , потому что .Net Framework выравнивает строки изображения с кратными 4 байтами . По этой причине при обработке изображений в виде байтов важно всегда сохранять значение «stride», в котором содержится фактическая ширина данных в байтах.

Еще одна важная вещь, о которой следует помнить, это то, что вам нужна палитра для индексированных изображений ; их пиксели - это не цвета, а ссылки на цветовую палитру. Без палитры они ничего не могут показать, и если вы проигнорируете палитру, они, вероятно, в конечном итоге получат цвета по умолчанию, которые Windows имеет для каждого из этих форматов пикселей, что обычно совсем не то, что нужно изображению.

И, наконец, если вы редактируете 1-битные или 4-битные данные в 8-битном массиве, вы должны быть уверены, что никогда не поместите данные в массив, который превышает максимально допустимый в исходном пиксельном формате. Таким образом, в данных 4bpp у вас никогда не должно быть байтов, значение которых превышает 0x0F, а в 1bpp у вас должны быть только значения 0 и 1 в байтах.

Функции GetImageData и BuildImage:

/// <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;
}

Функции ConvertTo8Bit и ConvertFrom8Bit:

/// <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 pixels that end up on the same byte
    Int32 parts = 8 / bitsLength;
    // Amount of bytes to write per line
    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 line
    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 относится к порядку блоков битов. Обычно байты 12 34 в 4bpp просто дают вам пиксели 1 2 3 4, в этом случае применяются правила с прямым порядком байтов (наибольшая математическая часть значения обрабатывается как первый пиксель). То же самое верно для 1bpp; значение 37 обычно дает пиксели 0 0 1 1 0 1 1 1. Но в некоторых нестандартных форматах файлов старых игр для DOS, над которыми я работал, это было не так (4bpp 12 34 давало бы пиксели 2 1 4 3), поэтому функция имеет этот параметр.

Аргумент start также существует, потому что данные, для которых я его использовал, были прочитаны из пользовательских форматов файлов. Обычно это всегда должно быть 0.

0 голосов
/ 30 июня 2018

Для форматов пикселей меньше 8 бит более одного пикселя упаковывается в один байт. Поэтому вы не можете использовать такой универсальный оператор для 8, 4 и 1-битных форматов:

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

Вместо этого, основываясь на формате пикселя, при извлечении данных пикселя должна быть рассчитана позиция бита в байте и извлечены соответствующие биты из байта - это будет либо «старший», либо «низкий» бит в случае 4-битных изображений или одного бита в случае 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 #, таких как здесь , здесь и здесь .

Собираем все это вместе с существующим кодом:

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;
        }
    }
}

Примечание: циклы for в SetPixel для извлечения индекса не совсем эффективны, поэтому, если вы часто используете эту функцию, вы можете захотеть реструктурировать код, чтобы он принимал значение индекса вместо цвет для проиндексированных изображений.

Наконец, чтобы использовать этот код, мы должны скопировать палитру перед использованием объекта locker для индексированных изображений, чтобы он выглядел примерно так:

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();
...