Recursive AI

Convert an image to grayscale in C#

All oneimage recipes

Load the image, call Grayscale(), and save. The gray is computed from Rec. 709 luminance in linear light, which keeps the brightness of the original, rather than from an average of the channels. Pass toGrayFormat: true to write a one-channel gray file. The program below converts NASA’s “Blue Marble” photograph (public domain).

Terminal
dotnet new console -o Grayscale && cd Grayscale
dotnet add package RecursiveAI.OneImage --version 1.10.4
curl -L -o photo.jpg \
  https://images-assets.nasa.gov/image/as17-148-22727/as17-148-22727~orig.jpg

OneImage needs the .NET 10 SDK. Put this in Program.cs and run dotnet run:

C#
using OneImage;
 
Image photo = Image.Load("photo.jpg");
 
// Gray, still stored as RGB (R = G = B).
photo.Grayscale().Save("gray-rgb.png");
 
// Gray in one 8-bit channel: a smaller file.
photo.Grayscale(toGrayFormat: true).Save("gray-8bit.png");
 
foreach (string file in new[] { "gray-rgb.png", "gray-8bit.png" })
{
    ImageInfo info = Image.Identify(file);
    long kb = new FileInfo(file).Length / 1024;
    Console.WriteLine($"{file}: {info.Frames[0].Format}, {kb:N0} KB");
}
Text
gray-rgb.png: Rgb/U8/None, 5,835 KB
gray-8bit.png: Gray/U8/None, 4,030 KB

Keep the default RGB layout when more steps follow, such as compositing a colored logo; ask for the gray format when the gray image is the output. Transparency is kept either way. The output format follows the extension, and the image streams through in tiles, so a large photo is never held whole in memory.

Linear light or stored values

The classic 0.3 R + 0.59 G + 0.11 B formula weighs the stored, gamma-encoded values, which makes saturated colors too dark. One pure red pixel shows the difference:

C#
using OneImage;
 
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];  // stored values
Console.WriteLine($"linear light: {linear.R}, stored values: {stored.R}");
Text
linear light: 127, stored values: 54

Pure red has about 21% of white’s luminance, which is a mid gray once encoded for the screen. Pass linear: false to match a tool that weighs stored values.

Pure black and white

For fax, thermal printers, e-ink or OCR, every pixel must be black or white. Threshold cuts at a level; BinaryDither keeps the tones as dot patterns:

C#
using OneImage;
 
Image photo = Image.Load("photo.jpg").Resize(width: 1000);
 
photo.Threshold(0.5).Save("threshold.png");   // 50% luminance and up is white
photo.BinaryDither(Dither.FloydSteinberg).Save("dither.png");
 
foreach (string file in new[] { "threshold.png", "dither.png" })
{
    Console.WriteLine($"{file}: {new FileInfo(file).Length / 1024:N0} KB");
}
Text
threshold.png: 36 KB
dither.png: 53 KB

For a phone photo of a page, where one level turns the shadowed part black, AdaptiveThreshold() compares each pixel with its neighborhood instead.

Limits

  • JPEG gains little from a one-channel file: its encoder already spends few bits on color. PNG and TIFF gain more.
  • Coming from System.Drawing? GetPixel/SetPixel loops and ColorMatrix code throw on Linux since .NET 6; see the “not supported on this platform” error.

Sepia(), BlackWhite() and Invert() are one call each; every color operation is in the guide.