How to Convert a Color Image to Grayscale in C#

By FoxLearn 1/16/2025 4:20:57 AM   54
Converting a color image to grayscale is a common task in image processing with C#.

Below are three different methods for converting a color image to grayscale, ranging from simple and slow approaches to more efficient solutions.

Slow and Simple Method

The first method is the easiest to implement and understand, but it’s the slowest in terms of performance.

It works by iterating over every pixel in the image, manually calculating the grayscale value based on the weighted sum of the RGB components.

public static Bitmap MakeGrayscale(Bitmap original)
{
    // Create a new bitmap with the same dimensions as the original image
    Bitmap newBitmap = new Bitmap(original.Width, original.Height);

    // Loop through each pixel in the original image
    for (int i = 0; i < original.Width; i++)
    {
        for (int j = 0; j < original.Height; j++)
        {
            // Get the color of the pixel at (i, j) from the original image
            Color originalColor = original.GetPixel(i, j);

            // Calculate the grayscale value using a weighted average of the RGB values
            // The weights (.3, .59, .11) approximate human eye sensitivity to red, green, and blue respectively
            int grayScale = (int)((originalColor.R * .3) + (originalColor.G * .59) + (originalColor.B * .11));

            // Create a new color where all RGB components are set to the grayscale value
            Color newColor = Color.FromArgb(grayScale, grayScale, grayScale);

            // Set the corresponding pixel in the new bitmap to the grayscale color
            newBitmap.SetPixel(i, j, newColor);
        }
    }

    // Return the new bitmap containing the grayscale image
    return newBitmap;
}

GetPixel and SetPixel are used to get and set pixel values, which is straightforward but inefficient for large images. This results in high processing time, especially with large images (e.g., 2048×2048 pixels).

Faster and More Complex Method

The second method improves on the first by using unsafe code to directly access memory, resulting in faster performance. It locks the image in memory, avoiding the inefficiencies of the GetPixel and SetPixel methods.

public static Bitmap MakeGrayscale2(Bitmap original)
{
    // Use the 'unsafe' keyword to allow pointer operations for faster access to image memory
    unsafe
    {
        // Create a new bitmap with the same dimensions as the original
        Bitmap newBitmap = new Bitmap(original.Width, original.Height);

        // Lock the original bitmap in memory to access pixel data directly
        // LockBits prevents the runtime from moving the image data in memory
        BitmapData originalData = original.LockBits(new Rectangle(0, 0, original.Width, original.Height),
                                                     ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
        // Lock the new bitmap in memory so we can write pixel data directly to it
        BitmapData newData = newBitmap.LockBits(new Rectangle(0, 0, original.Width, original.Height), 
                                                ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);

        // Pixel size is 3 bytes per pixel for 24bpp RGB format
        int pixelSize = 3;

        // Loop through each row of the image
        for (int y = 0; y < original.Height; y++)
        {
            // Get the address of the first pixel in the row for the original and new images
            byte* oRow = (byte*)originalData.Scan0 + (y * originalData.Stride);
            byte* nRow = (byte*)newData.Scan0 + (y * newData.Stride);

            // Loop through each pixel in the row
            for (int x = 0; x < original.Width; x++)
            {
                // Calculate the grayscale value using a weighted sum of RGB values
                // Using .11, .59, and .3 based on human eye sensitivity to blue, green, and red respectively
                byte grayScale = (byte)((oRow[x * pixelSize] * .11) +    // Blue component
                                        (oRow[x * pixelSize + 1] * .59) +  // Green component
                                        (oRow[x * pixelSize + 2] * .3));   // Red component

                // Set the new grayscale value for all three color channels (R, G, B)
                nRow[x * pixelSize] = grayScale;           // Blue
                nRow[x * pixelSize + 1] = grayScale;       // Green
                nRow[x * pixelSize + 2] = grayScale;       // Red
            }
        }

        // Unlock the bitmaps from memory to finalize the changes
        newBitmap.UnlockBits(newData);
        original.UnlockBits(originalData);

        // Return the new bitmap with the grayscale image
        return newBitmap;
    }
}

By using the unsafe keyword, we can directly manipulate the pixel data in memory using pointers.

The LockBits method locks the image data in memory, ensuring that the data remains stable and preventing potential issues caused by the .NET runtime moving the memory around.

Each pixel’s RGB values are accessed directly, and the grayscale value is calculated and set accordingly.

Short and Sweet Method

The third method leverages GDI+ for even faster performance and a simpler code structure. By using a ColorMatrix, we can perform the grayscale conversion with just a few lines of code.

public static Bitmap MakeGrayscale3(Bitmap original)
{
    // Create a new bitmap with the same dimensions as the original image
    Bitmap newBitmap = new Bitmap(original.Width, original.Height);

    // Create a Graphics object to draw on the new bitmap
    // This allows us to apply transformations such as color matrices to the image
    Graphics g = Graphics.FromImage(newBitmap);

    // Define a ColorMatrix that will be used to convert the image to grayscale
    // This matrix is used to adjust the RGB values based on human eye sensitivity
    ColorMatrix colorMatrix = new ColorMatrix(
        new float[][]
        {
            new float[] {.3f, .3f, .3f, 0, 0},   // Red component (weighted by 0.3)
            new float[] {.59f, .59f, .59f, 0, 0}, // Green component (weighted by 0.59)
            new float[] {.11f, .11f, .11f, 0, 0}, // Blue component (weighted by 0.11)
            new float[] {0, 0, 0, 1, 0},          // Alpha channel (unchanged)
            new float[] {0, 0, 0, 0, 1}           // No change to the alpha channel
        });

    // Create an ImageAttributes object to apply the ColorMatrix transformation
    ImageAttributes attributes = new ImageAttributes();
    
    // Set the ColorMatrix on the ImageAttributes object
    // This tells the graphics object how to process the image color channels
    attributes.SetColorMatrix(colorMatrix);

    // Use the Graphics object to draw the original image onto the new bitmap
    // The ColorMatrix transformation is applied during this drawing step
    g.DrawImage(original, 
                new Rectangle(0, 0, original.Width, original.Height), // Destination rectangle (same size as original)
                0, 0, original.Width, original.Height,               // Source rectangle (entire original image)
                GraphicsUnit.Pixel,   // Specify that the drawing unit is in pixels
                attributes);          // Apply the color matrix transformation

    // Dispose of the Graphics object to free resources
    g.Dispose();

    // Return the new bitmap containing the grayscale image
    return newBitmap;
}

The matrix here is set up to convert the image to grayscale by applying the appropriate weights to the RGB values.

The conversion is done using GDI+, where we create a Graphics object for the new image and apply the ColorMatrix through ImageAttributes.