Introduction
As far as I know, images cannot be converted into 8-bits per pixel grayscale bitmap images with GDI+. That's why I started working on this class. With this class, images can be converted into a palette based 8bpp grayscale image. The color palette contains 256 shades of gray.
Background
To create a bitmap file, one has to create 4 different parts of the file. Bitmap file header, DIB header, color palette and raw bitmap data.
Bitmap file header contains:
- Magic Number (2 bytes) at 0x0000 offset
- File Size (4 bytes) at 0x0002 offset
- Two 2 bytes long data, the values of which depend on the application that creates the file, at offsets 0x0006 and 0x0008
- Offset at which raw bitmap data can be found (4 bytes) at 0x000A
DIB header (Windows V3) contains:
- Size of the DIB header (4 bytes) at 0x000E. Its value is 40, i.e. the size of the DIB header is 40 bytes.
- Width of the image in pixel (4 bytes) at 0x0012
- Height of the image in pixel (4 bytes) at 0x0016
- The number of color planes being used (2 bytes) at 0x001A. Its value must be set to 1.
- Number of bits per pixel (2 bytes) at 0x001C. For the case of this particular project, its value is set to 8.
- Compression method being used (4 bytes) at 0x001E. In this case, the value is set to 0 which corresponds to no compression method being used.
- Size of the raw bitmap data (4 bytes) at 0x0022. More on this later.
- Horizontal and vertical resolutions of the image (pixels per meter). Each of these are 4 bytes and can be found at 0x0026 and 0x002A respectively. Their values are not really important.
- Number of colors in color palette (4 bytes) at 0x002E. In this particular project, 256 color (shades of gray) is used. Therefore its value is set to 256.
- Number of important colors used (4 bytes) at 0x0032. In this project, its value is set to 0 which corresponds to every color is important.
Color palette:
In this case, color palette containing 256 shades of gray is created [RGB(i,i,i) where i
ranges from 0 to 255]. At the end of each shade, one byte padding has to be added.
Raw bitmap data:
In raw bitmap data, pixels are mapped from left to right and from bottom to up. The left most pixel at the bottom of the bitmap image is stored first in raw bitmap data. At the end of each row of pixel necessary padding is added for 4 byte alignment, i.e the width of the bitmap file is divisible by 4 then no padding is added, if the remainder is 1, 2 or 3 then 3 byte, 2 byte or 1 byte padding is added respectively to maintain 4 byte alignment.
For calculating the length of raw bitmap data, one has to calculate the number of byte padding required after each row. Here is how to calculate the size of raw bitmap data:
raw data size (in bytes) = bitmap width* bitmap height*bytes per pixel+bitmap height* byte of padding at the end of each row
The RGB to Grayscale is converted by using the following formula:
Shade of gray = (int) (0.3 * R + 0.59 * G + 0.11 * B)
Say, after evaluating a pixel, the value of the shade of gray is found 100. It corresponds to the 101st color in the color palette, which should be RGB(100,100,100).
Code Explained
It's a static
class containing two public
methods:
bool CreateGrayBitmapFile(Image Image, string Path)
and
byte[] CreateGrayBitmapArray(Image Image)
The former saves the grayscale image to the specified path and returns true
if it successfully does so and the latter returns a byte array of the bitmap file, which can later be converted into an image by using ImageConverter class (included with the source) created by Rajan Tawate.
The static
class has 4 arrays for 4 parts of the BMP file.
static byte[] BMP_File_Header = new byte[14];
static byte[] DIB_header = new byte[40];
static byte[] Color_palette = new byte[1024];
static byte[] Bitmap_Data = null;
The internal method void create_parts(Image img)
populates the 4 above mentioned arrays. The array Bitmap_Data
is populated by using an internal method byte[] ConvertToGrayscale(Image Source)
which will be discussed later.
static void create_parts(Image img)
{
Bitmap_Data = ConvertToGrayscale(img);
Copy_to_Index(BMP_File_Header, new byte[]
{ (byte)'B', (byte)'M' }, 0);
Copy_to_Index(BMP_File_Header, BitConverter.GetBytes(BMP_File_Header.Length
+ DIB_header.Length + Color_palette.Length
+ Bitmap_Data.Length), 2);
Copy_to_Index(BMP_File_Header, new byte[] { (byte)'M', (byte)'C', (byte)'A',
(byte)'T' }, 6);
Copy_to_Index(BMP_File_Header, BitConverter.GetBytes(BMP_File_Header.Length
+ DIB_header.Length +
Color_palette.Length), 10);
Copy_to_Index(DIB_header, BitConverter.GetBytes
(DIB_header.Length), 0);
Copy_to_Index(DIB_header, BitConverter.GetBytes
(((Bitmap)img).Width), 4);
Copy_to_Index(DIB_header, BitConverter.GetBytes
(((Bitmap)img).Height), 8);
Copy_to_Index(DIB_header, new byte[] { (byte)1,
(byte)0 }, 12);
Copy_to_Index(DIB_header, new byte[] { (byte)8,
(byte)0 }, 14);
Copy_to_Index(DIB_header,
BitConverter.GetBytes(0), 16);
Copy_to_Index(DIB_header,
BitConverter.GetBytes(Bitmap_Data.Length), 20);
Copy_to_Index(DIB_header,
BitConverter.GetBytes(1000), 24);
Copy_to_Index(DIB_header,
BitConverter.GetBytes(1000), 28);
Copy_to_Index(DIB_header,
BitConverter.GetBytes(256), 32);
Copy_to_Index(DIB_header,
BitConverter.GetBytes(0), 36);
Color_palette = create_palette();
}
The internal method:
bool Copy_to_Index(byte[] destination, byte[] source, int index)
copies the contents of source array to the destination array at the index of the destination array. It is used to copy a smaller array to a larger array.
static bool Copy_to_Index(byte[] destination, byte[] source, int index)
{
try
{
for (int i = 0; i < source.Length; i++)
{
destination[i + index] = source[i];
}
return true;
}
catch
{
return false;
}
}
byte[] ConvertToGrayscale(Image Source)
method converts the RGB pixels into shades of gray and it maps the gray pixels left to right, bottom to top.
static byte[] ConvertToGrayscale(Image Source)
{
Bitmap source = (Bitmap)Source;
int padding = (source.Width % 4) != 0 ? 4 -
(source.Width % 4) : 0;
byte[] bytes = new byte[source.Width * source.Height + padding *
source.Height];
for (int y = 0; y < source.Height; y++)
{
for (int x = 0; x < source.Width; x++)
{
Color c = source.GetPixel(x, y);
int g = Convert.ToInt32(0.3 * c.R + 0.59 *
c.G + 0.11 * c.B);
bytes[(source.Height - 1 - y) * source.Width +
(source.Height - 1 - y) * padding + x] = (byte)g;
}
for (int i = 0; i < padding; i++)
{
bytes[(source.Height - y) * source.Width +
(source.Height - 1 - y) * padding + i] = (byte)0;
}
}
return bytes;
}
The color palette is created with the following code:
static byte[] create_palette()
{
byte[] color_palette = new byte[1024];
for (int i = 0; i < 256; i++)
{
color_palette[i * 4 + 0] = (byte)(i);
color_palette[i * 4 + 1] = (byte)(i);
color_palette[i * 4 + 2] = (byte)(i);
color_palette[i * 4 + 3] = (byte)0;
}
return color_palette;
}
Finally the code for the public
methods:
static public bool CreateGrayBitmapFile(Image Image, string Path)
{
try
{
create_parts(Image);
FileStream oFileStream;
oFileStream = new FileStream(Path, System.IO.FileMode.OpenOrCreate);
oFileStream.Write(BMP_File_Header, 0, BMP_File_Header.Length);
oFileStream.Write(DIB_header, 0, DIB_header.Length);
oFileStream.Write(Color_palette, 0, Color_palette.Length);
oFileStream.Write(Bitmap_Data, 0, Bitmap_Data.Length);
oFileStream.Close();
return true;
}
catch
{
return false;
}
}
static public byte[] CreateGrayBitmapArray(Image Image)
{
try
{
create_parts(Image);
byte[] bitmap_array = new byte[BMP_File_Header.Length + DIB_header.Length
+ Color_palette.Length + Bitmap_Data.Length];
Copy_to_Index(bitmap_array, BMP_File_Header, 0);
Copy_to_Index(bitmap_array, DIB_header, BMP_File_Header.Length);
Copy_to_Index(bitmap_array, Color_palette,
BMP_File_Header.Length + DIB_header.Length);
Copy_to_Index(bitmap_array, Bitmap_Data,
BMP_File_Header.Length + DIB_header.Length + Color_palette.Length);
return bitmap_array;
}
catch
{
return new byte[1];
}
}
Using the Code
Here's how to use the code:
static void Main(string[] args)
{
Console.WriteLine("Working...");
string path = Environment.CurrentDirectory + @"\new.jpg";
Image img;
try { img = Image.FromFile(path); }
catch (Exception ex)
{
Console.Write("An Error Occurred.\nError message: " + ex.ToString());
Console.ReadKey();
return;
}
GrayBMP_File.CreateGrayBitmapFile(img, Environment.CurrentDirectory + @"\gray1.bmp");
byte[] gray = GrayBMP_File.CreateGrayBitmapArray(img);
Image img2 = ImageConverter.byteArrayToImage(gray);
img2.Save(@"grey2.jpg", System.Drawing.Imaging.ImageFormat.Jpeg);
Console.WriteLine("Done.");
}