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.
dotnet add package RecursiveAI.OneImageThe 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
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
GetPixelandSetPixelonce 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:
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:
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:
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# covers both.
Grayscale keeps transparency
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:
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:
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
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# 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. Microsoft made System.Drawing.Common Windows-only in .NET 6, so that code throws on Linux and in most containers. Our page on the “System.Drawing.Common is not supported on this platform” error explains the options and shows how to port Bitmap code. Microsoft’s WPF how-to 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 for the tradeoffs, and the ImageSharp migration guide if you decide to move. For OneImage’s other operations and formats, start at the OneImage overview.