---
title: "Convert an image to grayscale in C#"
url: https://recursiveai.org/oneimage/articles/convert-image-to-grayscale-csharp/
description: "Convert images to grayscale in C# with one call: correct luminance, true 8-bit gray files, black and white with threshold or dithering, on any OS."
date: 2026-10-26
author: OneImage team
---

# Convert an image to grayscale in C#

Convert images to grayscale in C# with one call: correct luminance, true 8-bit gray files, black and white with threshold or dithering, on any OS.

October 26, 2026 · OneImage team

To convert an image to grayscale in C#, load it, call `Grayscale()` and save. With OneImage that is one line, it runs on Windows, Linux, macOS and in containers with no native libraries, and it computes luminance with the Rec. 709 weights in linear light instead of averaging the channels. This page also covers saving a true one-channel gray file, pure black and white for photos and scans, and converting a whole folder. Every sample was run against OneImage 1.10.2 on .NET 10.

*Terminal · NuGet*

```
dotnet add package RecursiveAI.OneImage
```

The examples use NASA’s public-domain “Blue Marble” photograph as `photo.jpg` (3000×3002), a transparent PNG logo and a scanned form.

## Convert an image to grayscale

*Program.cs · .NET 10*

```
using OneImage;
 
// Load a color photo, convert it to grayscale and save it.
Image.Load("photo.jpg").Grayscale().Save("photo-gray.jpg");
```

The result is a 3000×3002 grayscale JPEG. The format follows the file extension, so `photo-gray.png` or `photo-gray.webp` work the same way. Nothing is decoded until `Save`, and the image streams through in tiles, so a large photo is never held whole in memory.

`Grayscale()` weighs the channels by their Rec. 709 luminance (0.2126 R + 0.7152 G + 0.0722 B), the weights for the sRGB primaries most images use. Two things set it apart from the loops on older answers:

- It works in **linear light** by default: it decodes the sRGB curve, weighs the channels, then encodes again. That is the physically correct average of the light, and it keeps saturated colors from going too dark.
- It processes the image tile by tile inside the library, instead of calling `GetPixel` and `SetPixel` once per pixel from your code.

## Linear light or stored values

Many editors and the classic `(R * 0.3) + (G * 0.59) + (B * 0.11)` formula weigh the stored, gamma-encoded values. The difference is largest on saturated colors:

*Program.cs · .NET 10*

```
using OneImage;
 
// One pure red pixel.
Image red = Image.FromPixels<Rgb24>(new[] { new Rgb24(255, 0, 0) }, 1, 1);
 
Rgb24 linear = red.Grayscale().ToArray<Rgb24>()[0];               // the default
Rgb24 stored = red.Grayscale(linear: false).ToArray<Rgb24>()[0];  // weigh the stored values
Console.WriteLine($"linear light: {linear.R}, stored values: {stored.R}");
```

This prints `linear light: 127, stored values: 54`. Pure red has a luminance of about 21% of white, and 21% of the light is a mid gray (127) once it’s encoded for the screen. Weighing the encoded value gives 54, a much darker gray. Use the default when you want the gray to keep the brightness of the original. Pass `linear: false` when you need to match another tool that weighs stored values.

## Save a true 8-bit grayscale file

By default `Grayscale()` keeps the pixel format, so an RGB image stays RGB with R = G = B. That keeps later steps such as compositing a colored logo simple. When the gray image is the final output, ask for a one-channel format and the file gets smaller:

*Program.cs · .NET 10*

```
using OneImage;
 
Image photo = Image.Load("photo.jpg");
 
// The same gray picture in two layouts: RGB with R = G = B, or one gray channel.
photo.Grayscale().Save("gray-rgb.png");
photo.Grayscale(toGrayFormat: true).Save("gray-8bit.png");
 
foreach (string file in new[] { "gray-rgb.png", "gray-8bit.png" })
{
    ImageInfo info = Image.Identify(file);
    Console.WriteLine($"{file}: {info.Frames[0].Format}, {new FileInfo(file).Length / 1024:N0} KB");
}
```

On the Blue Marble photo this printed:

*Output · 1.10.2*

```
gray-rgb.png: Rgb/U8/None, 4,963 KB
gray-8bit.png: Gray/U8/None, 3,985 KB
```

The one-channel PNG is about 20% smaller than the RGB one. JPEG gains less, because its encoder already spends few bits on color: the same photo came out at 859 KB as RGB and 838 KB as one-channel gray, at the default quality of 75. If you need a smaller JPEG, quality and size matter far more. Our guide to [compressing images in C#](https://recursiveai.org/oneimage/articles/compress-image-csharp/) covers both.

## Grayscale keeps transparency

*Program.cs · .NET 10*

```
using OneImage;
 
// Grayscale keeps the alpha channel, so transparent logos stay transparent.
Image logo = Image.Load("logo.png");
Image gray = logo.Grayscale();
Console.WriteLine($"{logo.Format} -> {gray.Format}");
gray.Save("logo-gray.png");
```

Alpha is left alone, so a transparent logo stays transparent and only its colors turn gray. The 600×300 logo printed `Rgb/U8/Straight -> Rgb/U8/Straight`, and its alpha channel came through unchanged. With `toGrayFormat: true` the result is gray plus alpha.

## Convert an image to black and white

“Black and white” means two different things. A grayscale photo is often called black and white. Pure black and white means every pixel is either black or white, which is what fax, thermal printers, e-ink displays and some OCR pipelines want. OneImage has both kinds:

*Program.cs · .NET 10*

```
using OneImage;
 
Image photo = Image.Load("photo.jpg");
 
// Hard black and white: luminance at or above 50% becomes white.
photo.Threshold(0.5).Save("photo-threshold.png");
 
// Black and white with dithering, so the tones survive as dot patterns.
photo.BinaryDither(Dither.FloydSteinberg).Save("photo-dither.png");
```

`Threshold(0.5)` turns every pixel at or above 50% luminance white and the rest black. The level is a number from 0 to 1, so raise it to get more black. `BinaryDither` keeps the impression of the tones as dot patterns. Floyd–Steinberg spreads each pixel’s rounding error to its neighbors, and the ordered Bayer patterns (the default is `Bayer8x8`) lay down a regular grid. Both PNGs were written as two-color palette images, at 178 KB for the threshold and 629 KB for the dithered version.

### Black and white for scanned documents

A phone photo of a page is rarely lit evenly. One global threshold then turns the shadowed part of the page black. `AdaptiveThreshold()` compares each pixel with the average brightness around it, so it adapts to the lighting:

*Program.cs · .NET 10*

```
using OneImage;
 
Image scan = Image.Load("scan.png");
 
// One global level: the shadowed corner turns solid black.
scan.Threshold(0.5).Save("scan-threshold.png");
 
// A level per neighborhood: text stays black and paper stays white everywhere.
scan.AdaptiveThreshold().Save("scan-adaptive.png");
```

Our test page was a tax form rendered at 100 dpi (850×1100) and darkened toward the bottom-right corner, the way a phone shadow falls. With `Threshold(0.5)` that corner came out as a solid black triangle. With `AdaptiveThreshold()` the whole page came out as black text on white paper, in a 28 KB PNG.

## Convert a folder of images to grayscale

*Program.cs · .NET 10*

```
using OneImage;
 
Directory.CreateDirectory("gray");
 
foreach (string path in Directory.EnumerateFiles("photos"))
{
    if (!Image.TryLoad(path, out Image? image))
    {
        continue;   // not an image, or damaged
    }
 
    string name = Path.GetFileNameWithoutExtension(path);
    image.Grayscale().Save(Path.Combine("gray", name + ".jpg"));
}
```

`Image.TryLoad` returns `false` for files that aren’t images or are damaged, so a stray text file or a broken upload is skipped instead of stopping the batch. Phone photos come out upright, because `Load` applies the EXIF orientation first. Our portrait test photo, stored sideways at 1200×800, came out as an 800×1200 gray JPEG. See [EXIF orientation in C#](https://recursiveai.org/oneimage/articles/exif-orientation-csharp/) for why that matters.

Gray is also just one step in a pipeline. `Image.Load("in.jpg").Resize(width: 800).Grayscale().Save("out.webp")` resizes, converts and encodes in one pass. Related looks are one call each: `Sepia()`, a high-contrast `BlackWhite()`, and `Invert()`.

## Grayscale with System.Drawing, WPF and other libraries

Most grayscale answers for C# use `System.Drawing`, either looping with `GetPixel` and `SetPixel` or drawing through a `ColorMatrix`, as in the [top Stack Overflow answers](https://stackoverflow.com/questions/2265910/convert-an-image-to-grayscale). Microsoft made `System.Drawing.Common` [Windows-only in .NET 6](https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/system-drawing-common-windows-only), so that code throws on Linux and in most containers. Our page on the [“System.Drawing.Common is not supported on this platform” error](https://recursiveai.org/oneimage/articles/system-drawing-common-not-supported/) explains the options and shows how to port `Bitmap` code. Microsoft’s [WPF how-to](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/how-to-convert-an-image-to-greyscale) uses `FormatConvertedBitmap`, which belongs to WPF desktop apps rather than services.

If you already use ImageSharp or SkiaSharp, see our [comparison of .NET image libraries](https://recursiveai.org/oneimage/articles/imagesharp-alternatives/) for the tradeoffs, and the [ImageSharp migration guide](https://recursiveai.org/oneimage/articles/migrate-from-imagesharp/) if you decide to move. For OneImage’s other operations and formats, start at the [OneImage overview](https://recursiveai.org/oneimage/).
