How can I work with 1-bit and 4-bit images?
Asked Answered
D

2

0

BitmapLocker class is intended for fast read/write of pixels in a Bitmap image file.

But, Color GetPixel(int x, int y) and void SetPixel(int x, int y, Color c) cannot handle 1-bit and 4-bit images.

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

For instance, the following code displays a fully black output:

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

How can I correct this code so that it can handle 1-bit and 4-bit images?

.

.

Sample Input

1-bit monochrome and 4-bit color
enter image description here enter image description here

Demonolatry answered 27/6, 2018 at 21:53 Comment(3)
Can you explain to use what BitmapLocker is trying to do?Lepage
4-bit and 8-bit color images are indexed images? As in they use a color map? Handling those is very different from handling grey-scale or RGB images.Urrutia
@CrisLuengo Actually, in the .Net framework, greyscale images are treated as indexed.Janik
G
8

For pixel formats smaller than 8 bits, more than one pixel is packed into a single byte. Therefore you can not have a catch-all statement like this for 8, 4 and 1-bit formats:

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

Instead, based on the pixel format, when retrieving the pixel data the bit position in the byte has to be calculated and appropriate bits extracted from the byte -- this would be either "high" or "low" bits in the case of 4-bit images or the single bit in case of 1-bit images. Conversely, when setting the pixel data only certain bits in the byte (based on the pixel format) need to be changed.

Suppose we have a 4-bit format image. The image data may look something like this:

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

This format packs two pixels per byte. Therefore, when retrieving pixel data, we first compute the bit index for the pixel:

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

Stride is the number of bytes in a single row, so just multiply that by height * 8 (for 8 bits in a byte) and add the width * ColorDepth (for number of bits per pixel).

Then we need to figure out whether we want to retrieve the first four bits in the byte or the last four bits. For that, we simply compute bitindex mod 8. Obviously if the pixel starts with the byte, this will be 0 (for example, 8 mod 8 = 0), otherwise it will be 4. Based on that, if we want the first four bits, we shift the byte by four. C# zeroes out the first four bits:

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

On the other hand, if we want the last four bits, we AND the image data byte with a byte that has the first four bits zeroed out:

+---+---+---+---+---+---+---+---+  
| 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 |  
+---+---+---+---+---+---+---+---+

In code, all this looks something like:

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

For 1-bit, monochrome images, we want to get at the single bit. For that, we AND the image data byte with a byte that has all the other bits zeroed out (the "mask"). For example, if we want to get at the bit at index 5, we would do this:

+---+---+---+---+---+---+---+---+  
| 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 |  
+---+---+---+---+---+---+---+---+

If the result is zero, then we know that the bit is zero, otherwise the bit is "set". In code:

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

Once we have retrieved the pixel data, let's retrieve the actual color since the GetPixel function returns a Color object. For 8-, 4- and 1-bit images the pixel data actually represents an index into a color palette. A color palette looks something like this:

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

We have access to the color palette, so to retrieve the color:

clr = Palette.Entries[c];

Where c is the retrieved pixel data.

Something similar is done for setting pixel data. There is plenty of information on bit manipulation in C#, such as here, here and here.

Putting it all together, keeping with your existing code:

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

Note: the for loops in SetPixel to retrieve the index are not exactly efficient so if you're using that function a lot you may want to restructure the code so it takes in an index value instead of a color for indexed images.

Finally, in order to use this code, we must copy the palette before using the locker object for indexed images, so it would look something like this:

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();
Guttering answered 30/6, 2018 at 2:51 Comment(6)
Could you kindly explain what logic/computation you have used in both cases?Demonolatry
"For 8 bpp get color value (Red, Green and Blue values are the same)" This is completely false. Indexed formats are not just grayscale. They are indexed, which means the values you find on the image are indices rather than colour values, and their colour should be taken from the specified index on the colour palette. You can perfectly have a 1bpp image where the two colours are green and blue.Janik
@Guttering You could've easily seen that from the fact his 4-bit example isn't grayscale, btw.Janik
@Janik First, those are not my words, that's out of OP's code, check the question. Second, you may be misinterpreting the intent here, which is simply to jam these values into a Color object since that's what the function returns. (that's why I said there are many ways of doing this but I will keep with the existing code). Of course the color depends on the palette.Guttering
@Guttering True, that is OP's code (or rather, the code he got from CodeProject)... but you copied it without correction. I fail to see the use of "giving a Color object" if that color doesn't actually match the pixel in the image. You gave useful information... but it didn't solve the issue of getting the right colours.Janik
OK you make a good point, I can see how it can get confusing. I added some information on color palettes and updated to code to reflect it.Guttering
J
3

1-bit and 4-bit content is rather annoying to work with. For that reason, any indexed data I work with I simply convert to a more convenient 1 byte per pixel (8bpp) for processing, using a ConvertTo8Bit and ConvertFrom8Bit function.

They work in tandem with a GetImageData function to get the bytes out of an image, and a BuildImage function to build a new Bitmap out of the bytes.

One important thing to remember about images in general is that the width of a line in pixels is not necessarily the same as the amount of bits multiplied by the width. First of all, because for 1bpp or 4bpp you can have an excess anyway to get to the next full byte, and secondly, because the .Net framework aligns image lines to multiples of 4 bytes. For this reason, when handling images as bytes, it's important to always keep a stride value around which contains that actual data width in bytes.

Another important thing to keep in mind is that you need a palette for indexed images; their pixels are not colours but references to the colour palette. Without a palette they can't show anything, and if you ignore the palette they'll probably end up with the default colours Windows has for each of these pixel formats, which is usually not at all what the image needs.

And finally, if you edit 1-bit or 4-bit data in an 8-bit array, you have to make sure to never put data into the array that exceeds the maximum allowed in your original pixel format. So in 4bpp data, you should never have bytes that have a value higher than 0x0F, and in 1bpp, you should really only have the values 0 and 1 in your bytes.

The GetImageData and BuildImage functions:

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

The ConvertTo8Bit and ConvertFrom8Bit functions:

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

Note that the bigEndian argument refers to the order of the blocks of bits. Normally, the bytes 12 34 in 4bpp will simply give you pixels 1 2 3 4, in which case big-endian rules apply (the mathematically-biggest part of the value is handled as the first pixel). The same is true for 1bpp; a value of 37 will normally give pixels 0 0 1 1 0 1 1 1. But in some custom file formats of old DOS games I worked on, this was not the case (4bpp 12 34 would give pixels 2 1 4 3), hence why the function has that parameter.

The start argument, similarly, is there because the data I used this for was read from custom file formats. Normally this should always just be 0.

Janik answered 1/7, 2018 at 14:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.