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).
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:
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"); }
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:
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}");
linear light: 127, stored values: 54Pure 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:
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"); }
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/SetPixelloops andColorMatrixcode 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.