3

FIRST OF ALL


First of all, please note that the answer given in this question will not work for all the grayscale images, and also note that the accepted answer in this other question does not explain at all how to determine if an image is grayscale, but anyways it does not fit my needs because it seems to only cover JPEG and TIFF images and assuming they will have EXIF metadata and the required fields inside it. (I can't understand why people determined that the first question I linked is a "duplicated" of that second question I linked too...)

And finally, this last accepted answer lacks of a working and demonstrative code example, but anyways that would not help because the author refers to the slow and deprecated methodology using Bitmap.GetPixel() function, but we should use Bitmap.LockBits() function instead for the higher performance benefit.

SCENARIO


I have some GIF, JPG, BMP and PNG images for which I require to determine whether they are Grayscale images or they are not. For the GIF files I only care about analyzing the first frame.

I'm not much experienced/aware about image's data structures, pixel color bits and those things, I only know the very basics. So, if I'm missing important info and I should give any kind of info of the images I will test, then just ask me, but anyways take into account that I would like to create a generic soluton for "all" kind of images, well, not all, but at least these formats: BMP, JPG, GIF and PNG.

Of those image formats mentioned, my highest priority are the GIF images, with this I will mean that if the methodology to be able determine whether a GIF image is Grayscale can't be the same methodology to analyze other kind of images, then I will accept an answer that covers only GIF image pixel treatment.

QUESTION


I think my needs are clear: how can I determine whether an image is Grayscale?.

In case of it was not clear at all, to avoid that I could make that you could waste your time:

  • The solution must work at least for GIF images. (remember, I only care about the first frame in the GIF), but if the provided solution can work too for BMP, JPG and PNG then of course its always better.

  • The solution must care about PixelFormat.Format32bppRgb grayscale images.

  • The solution must not use Bitmap.GetPixel() function, it must use Bitmap.LockBits() instead.

  • I'm not asking for explanations, pseudo-codes neither external links to documentation about image structures/formats/pixels etc, I'm asking for a working code example (of course always better if the author covers the image structures/pixels technicisms to provide essential explanations in addition to the code).

  • In C# or VB.NET, the choice does not matter.

RESEARCH


This is what I tried to do so far. I'm stuck trying to understand the point to determine whether a image is Grayscale or not, also I'm not sure whether my conditions with the bytesPerPixel variable are proper and also whether my RGB value assignations are correct since as I said from the start I'm not a image-processing expert so I could have missed important things...

VB.NET

Public Shared Function IsImageGrayScale(ByVal img As Image) As Boolean

    Select Case img.PixelFormat

        Case PixelFormat.Format16bppGrayScale
            Return True

        Case Else
            Dim pixelCount As Integer = (img.Width * img.Height)
            Dim bytesPerPixel As Integer = (Image.GetPixelFormatSize(img.PixelFormat) \ 8)

            If (bytesPerPixel <> 3) AndAlso (bytesPerPixel <> 4) Then
                Throw New NotImplementedException(message:="Only pixel formats that has 3 or 4 bytes-per-pixel are supported.")

            Else
                Dim result As Boolean

                ' Lock the bitmap's bits.
                Dim bmp As Bitmap = DirectCast(img, Bitmap)
                Dim rect As New Rectangle(Point.Empty, bmp.Size)
                Dim data As BitmapData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat)

                ' Get the address of the first line.
                Dim ptr As IntPtr = data.Scan0

                ' Declare an array to hold the bytes of the bitmap. 
                Dim numBytes As Integer = (data.Stride * bmp.Height)
                Dim rgbValues As Byte() = New Byte(numBytes - 1) {}

                ' Copy the RGB values into the array.
                Marshal.Copy(ptr, rgbValues, 0, numBytes)

                ' Unlock the bitmap's bits.
                bmp.UnlockBits(data)

                ' Iterate the pixels.
                For i As Integer = 0 To (rgbValues.Length - bytesPerPixel) Step bytesPerPixel

                    Dim c As Color =
                        Color.FromArgb(red:=rgbValues(i + 2),
                                       green:=rgbValues(i + 1),
                                       blue:=rgbValues(i))

                    ' I don't know what kind of comparison I need to do with the pixels, 
                    ' so I don't know how to proceed here to determine whether the image is or is not grayscale.
                    ' ...

                Next i

                Return result
            End If

    End Select

End Function

C# (code conversion, untested)

public static bool IsImageGrayScale(Image img) {

    switch (img.PixelFormat) {

        case PixelFormat.Format16bppGrayScale:
            return true;

        default:
            int pixelCount = (img.Width * img.Height);
            int bytesPerPixel = (Image.GetPixelFormatSize(img.PixelFormat) / 8);

            if ((bytesPerPixel != 3) && (bytesPerPixel != 4)) {
                throw new NotImplementedException(message: "Only pixel formats that has 3 or 4 bytes-per-pixel are supported.");

            } else {
                bool result = false;

                // Lock the bitmap's bits.
                Bitmap bmp = (Bitmap)img;
                Rectangle rect = new Rectangle(Point.Empty, bmp.Size);
                BitmapData data = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat);

                // Get the address of the first line.
                IntPtr ptr = data.Scan0;

                // Declare an array to hold the bytes of the bitmap. 
                int numBytes = (data.Stride * bmp.Height);
                byte[] rgbValues = new byte[numBytes];

                // Copy the RGB values into the array.
                Marshal.Copy(ptr, rgbValues, 0, numBytes);

                // Unlock the bitmap's bits.
                bmp.UnlockBits(data);

                // Iterate the pixels.
                for (int i = 0; i <= rgbValues.Length - bytesPerPixel; i += bytesPerPixel) {

                    Color c = Color.FromArgb(red: rgbValues[i + 2], 
                                             green: rgbValues[i + 1], 
                                             blue: rgbValues[i]);

                    // I don't know what kind of comparison I need to do with the pixels, 
                    // so I don't know how to proceed here to determine whether the image is or is not grayscale.
                    // ...

                }

                return result;
            }
    }
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
ElektroStudios
  • 19,105
  • 33
  • 200
  • 417
  • 1
    You're going about it wrong. There is no reason to look at the actual raster map. Look at the header instead. https://msdn.microsoft.com/en-us/library/system.drawing.image.pixelformat(v=vs.110).aspx –  Mar 25 '18 at 09:18
  • The grey scale means, that `R == B == G`. *(Or not?)* – Julo Mar 25 '18 at 09:21
  • @MickyD thanks for comment, but the link you provided exposes a example to "demonstrates how to construct a new bitmap from a file, using the GetPixel and SetPixel methods to recolor the image", I don't get you in what manner that example would help me. In the code example I only see a demonstration of how to iterate all the pixels of an image using **GetPixel** function, and **SetPixel** to reassign pixel colors. If you only linked that article because PixelFormat property, I explained Format16bppGrayScale is not the way since will not be that for all grayscale images... – ElektroStudios Mar 25 '18 at 09:26
  • [BitmapDecoder](https://msdn.microsoft.com/en-us/library/system.windows.media.imaging.bitmapdecoder(v=vs.110).aspx) in [System.Windows.Media.Imaging](https://msdn.microsoft.com/en-us/library/system.windows.media.imaging(v=vs.110).aspx) can handle this, but it's not clear if you can handle it. (Winforms, WPF, UWP?). However, you question is non-trivial. Starting from the definition of what is actually Grayscale. – Jimi Mar 25 '18 at 09:46
  • 1
    `I don't know what kind of comparison I need to do with the pixels` - True grayscale is when, like Julo said, a pixel's `R`, `G` and `B` values are the same. Thus: `If Not (c.R = c.G AndAlso c.G = c.B) Then Return False` – Visual Vincent Mar 25 '18 at 10:00
  • Do you want this in vb or .net – TheGeneral Mar 25 '18 at 10:39
  • Try this then https://msdn.microsoft.com/en-us/library/system.drawing.imaging.pixelformat(v=vs.110).aspx. Its obvious that you didnt bother to look up the enum in the first link –  Mar 25 '18 at 12:11
  • It is absolutely pointless to determine whether or not an image is grayscale; indexed colour; true colour; or HDR by looking at the raster map by ~GetPixel()`; `LockBits` et al. **You must look at the image header first**. By examing the header, it tells you how to interpret the raster map and not the other way around. Now whether you use `Image.PixelFormat` is another story –  Mar 25 '18 at 12:16
  • @Jimi Thanks. I'm on WinForms but I have no objection to reference the WPF assemblies in order to use that class, I will check the MSDN documentation later, but just one question: you suggested that class as a solution to determine grayscale, or as a performance improvement to replace Bitmap.LockBits()? (I never used that class neither the majority of WPF classes) – ElektroStudios Mar 25 '18 at 12:47
  • @TheGeneral If you take a look at the question tags you will see: C#, VB.NET and .NET. I also specified this in the question: "In C# or VB.NET, the choice does not matter.". thanks for comment – ElektroStudios Mar 25 '18 at 12:50
  • @MickyD Ok then... I have to check the image header, but what really means that?. I suppose each image format has a different header... a different data structure, and I don't know what I need to check for a GIF image, could you please give me some hint?. You linked again an article to the PixelFormat enumeration but I don't get you what do you mean, because I have grayscale images that returns Format32bppArgb pixel format, for example. thanks for comment. – ElektroStudios Mar 25 '18 at 12:54
  • @Visual Vincent and Julo: thanks for clarifying that point. now I have that thing clear, however I think that color evaluation will not be enough to solve this, as I'm seeying in the other comments... – ElektroStudios Mar 25 '18 at 13:01
  • All of you are making the mistake that R = G = B = always greyscale. You are all making the assumption that the bits per pixel is always the same. What about [12 bit HDR greyscale](https://www.nvidia.com/content/quadro_fx_product_literature/TB-04631-001_v05.pdf)? Common in diagnostic imaging medical applications, instead of 8 bit primaries there may be a single 12 bit vale transmitted over the "video wire" and as far as GPU shaders are concerned, still dealing with 8 bit RGB channels. You also say you want to cover GIF, GIF had its beginnings as representing images via palettes. –  Mar 25 '18 at 13:10
  • 1
    The code has been update, including the statistical informations, as promised. See the "manual" about what's been implemented. I've tested it with more images. It looks OK and pretty much complete. I didn't notice false positives/negatives. Also, I've decided to add an interface to the class to explain/test how it works. If you're interested, I'll let you know when it's ready (tomorrow, I think) and I'll post a link here. If you think that something's missing or not working as expected, let me know. (Some Diamond wiped us out! :) – Jimi Mar 28 '18 at 21:20

1 Answers1

7

What I propose is using Presentation Core's System.Windows.Media.Imaging, which exposes a the abstract BitmapDecoder class, base of all decoders that Windows Imaging directly supports:

System.Windows.Media.Imaging.BmpBitmapDecoder
System.Windows.Media.Imaging.GifBitmapDecoder
System.Windows.Media.Imaging.IconBitmapDecoder
System.Windows.Media.Imaging.JpegBitmapDecoder
System.Windows.Media.Imaging.LateBoundBitmapDecoder
System.Windows.Media.Imaging.PngBitmapDecoder
System.Windows.Media.Imaging.TiffBitmapDecoder
System.Windows.Media.Imaging.WmpBitmapDecoder

When decoding an Image file stream, the correct decoder is cast from the abstract class to the specific class.

The decoded Image frames are cast to a BitmapFrame Class, whose members translate to a BitmapSource class, which references all the decoded informations about the Image Stream.

Of interest, in this case, is the BitmapSource.Format property, which exposes a System.Windows.Media.PixelFormat Structure and its enumeration of recognized formats.

See also PixelFormats Properties

These formats include:

PixelFormats.Gray32Float
PixelFormats.Gray16
PixelFormats.Gray8
PixelFormats.Gray4
PixelFormats.Gray2

These flags can be tested as usual.


This class can be used to collect informations about a Bitmap format.
I've included a Property, IsGrayscale, which returns the result of the test of the Image PixelFormat, using the PixelFormats previously listed.

The Image Format is referenced by the BitmapInfo.Format BitmapInfo.Metadata.Format properties (different sources, for comparison.
The other properties are quite self-explanatory.

A Project that implements this class must reference:

PresentationCore
System.Xaml
WindowsBase

Properties:

ImageSize            (Size)          => Size of the Image
Dpi                  (Size)          => DpiX and DpiY of the Image
PixelSize            (Size)          => Size in Pixels ot the Image
Masks                (List)          => List of Byte Masks
BitsPerPixel         (int)           => Bits per Pixel
PixelFormat          (PixelFormat)   => Pixel format as reported by the Decoder
ImageType            (string)        => Textual expression of the image format (GIF, JPG etc.)
HasPalette           (bool)          => The Image has a Palette
Palette              (BitmapPalette) => Palette representation of the Image Colors
HasThumbnail         (bool)          => The Image includes a Thumbnail image
Thumbnail            (BitmapImage)   => The Image Thumbnail, in BitmapImage format
Frames               (int)           => Number of frames. Animated Images are represented by a sequence of frames
FramesContent        (FramesInfo)    => Informations about all frame included in this Image
IsMetadataSuppported (bool)          => The Image has Metadata informations
Metadata             (MetadataInfo)  => Class referencing all the Metadata informations a Image contains
AnimationSupported   (bool)          => This Format supports frame Animations
Animated             (bool)          => The Image is a timed sequence of frames

Methods:

public enum DeepScanOptions : int  {
    Default = 0,
    Skip,
    Force
}

public bool IsGrayScale(DeepScanOptions DeepScan)

Checks whether the Image PixelFormat is to be considered GrayScale, given the image internal Palette. The DeepScanOptions enumerator is used to determine how the scan is performed.
More details in the Sample Usage part.


public enum GrayScaleInfo : int {
    None = 0,
    Partial, 
    GrayScale,
    Undefined
}

public ImagingBitmapInfo.GrayScaleInfo IsGrayScaleFrames()

Reports the status of the Frames Palettes. It might return:

None: The Image has no Grayscale Frames
Partial: Some Frames are GrayScale
GrayScale: All Frames have a GrayScale Palette
Undefined: The Image probably has no Palette Information. The Image pixel format is reported by the PixelFormat property


public ImagingBitmapInfo.GrayScaleStats GrayScaleSimilarity();

This method performs a statistical evaluation (Average, (Sum(Min) <=> Sum(Max)), considering the Colors of all the internal Palettes of an Image, to verify how much the internal colorific representation can be assimilated to a GrayScale pattern.
It returns a ImagingBitmapInfo.GrayScaleStats, which exposes these Properties:

int Palettes: Number of Palette evaluated
float AverageMaxDistance: Average distance (Max) between RGB components
float AverageMinDistance: Average distance (Min) between RGB components
float AverageLogDistance: Average logical distance between RGB components
float GrayScalePercent: Percentage of similarity
float GrayScaleAveragePercent: Percentage of logical similarity

List<FrameStat> PerFrameValues: Class that reports the calculated results for each Palette entry. It exposes these Properties:

int ColorEntries: Number of Colors in the current Palette
float DistanceMax: Distance (Max) between RGB components
float DistanceMin: Distance (Min) between RGB components
float DistanceAverage: Average distance between RGB components


public void FrameSourceAddRange(BitmapFrame[] bitmapFrames)

Inserts all Image Frames information in a FramesInfo Class.
It's used internally, but can be filled manually when an instance of the main class, ImagingBitmapInfo, is created. Exposes these properties:

FramesTotalNumber: Total number of Frames included in the Image
FramesColorNumber: Number of frames that have a Color Palette
FramesGrayscaleNumber: Number of GrayScale Frames
FramesBlackWhiteNumber: Number of B&W Frames

List<Frames>: Class List of all frames. The FramesInfo Class object Exposes these properties:

FrameSize: Size of the Frame
FrameDpi: DpiX and DpiY of the Frame
PixelFormat: PixelFormat of the Frame
IsColorFrame: The frame has a Color Palette
IsGrayScaleFrame: The frame has a GrayScale Palette
IsBlackWhiteFrame: The frame has a B&W Palette


public System.Drawing.Bitmap ThumbnailToBitmap()

Converts a System.Windows.Media.Imaging BitmapImage in a System.Drawing Bitmap format that can be used in WinForms Controls/Classes. (Not properly tested at this time).


Sample usage:
The main class, ImagingBitmapInfo, is initialized passing to the BitmapFormatInfo() method a File Path or a File Stream.

ImagingBitmapInfo BitmapInfo = BitmapFormatInfo(@"[ImagePath]");
//or 
ImagingBitmapInfo BitmapInfo = BitmapFormatInfo([FileStream]);

To verify whether the Image has a GrayScale PixelFormat, call the IsGrayScale(ImagingBitmapInfo.DeepScanOptions) method, specifying how this information must be retrieved.

ImagingBitmapInfo.DeepScanOptions.Default
The class decides, based on the Image Pixel Format, whether to perform a Deep Scan of the image Color Palette (if a Palette is present). If the Pixel Format already reports a GrayScale Image (e.g. PixelFormats.Gray32Float, PixelFormats.Gray16 etc.), a Deep scan is not performed. If the Pixel Format is an Indexed one, the scan is performed; if the PixelFormat is Color format, the scan is not performed.

Note that some Images (Gifs, mostly) may report a Color PixelFormat, while the inner format (Palette) might be GrayScale.

ImagingBitmapInfo.DeepScanOptions.Force
Instructs to execute a Deep Scan of the Palettes of all Frames, no matter what PixelFormat is reported by the Image decoder.
Used to discover if a reported Color Image has one or more GrayScale Frames.

ImagingBitmapInfo.DeepScanOptions.Skip
Instructs to not perform a Deep scan of the Palettes, even if it would be normally performed, given the smelly Pixel Format.

System.Windows.Media.PixelFormat pixelFormat = BitmapInfo.PixelFormat;
bool BitmapIsGrayscale = BitmapInfo.IsGrayScale(ImagingBitmapInfo.DeepScanOptions.Force);

If the results are different from what is expected, a complete check of the Image Frames PixelFormat can be performed calling:

ImagingBitmapInfo.GrayScaleInfo GrayScaleFrames = BitmapInfo.IsGrayScaleFrames();

This method performs a complete check of all Frames and reports if any of the internal Frames have a GrayScale PixelFormat. The result can be one of the GrayScaleInfo enumerator values:
None, Partial, GrayScale, Undefined.
If the result is GrayScale, all internal Frames have a GrayScale PixelFormat.
Undefined means that the Image has no Palette informations.

To create a statistical representation of Grayscale similarity of an Image Palettes' Color entries, call the GrayScaleSimilarity() method:

ImagingBitmapInfo.GrayScaleStats Stats = BitmapInfo.GrayScaleSimilarity();

float GrayScalePercent = Stats.GrayScalePercent
float RGBAverageDistancePercent = Stats.GrayScaleAveragePercent
float RGBPatternMaxDistance = Stats.AverageMaxDistance

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

public class ImagingBitmapInfo
{
    Dictionary<string, string> ImgTypeLongNames;
    readonly FramesInfo framesInfo = new FramesInfo();

    public ImagingBitmapInfo(FileStream stream)
    {
        FillLongNameTable();

        var coder = BitmapDecoder.Create(
            stream,
            BitmapCreateOptions.PreservePixelFormat,
            BitmapCacheOption.Default);

        BitmapSource source = coder.Frames[0];
        var metadata = (BitmapMetadata)source.Metadata;
        try {
            ImageType = ImgTypeLongNames[metadata.Format.ToUpperInvariant()];
            Metadata = new MetadataInfo(metadata);
        }
        catch (NotSupportedException) {
            Metadata = null;
            IsMetadataSupported = false; 
        }

        Frames = coder.Frames.Count();
        if (Frames > 0) FrameSourceAddRange(coder.Frames.ToArray());

        Animated = source.HasAnimatedProperties;
        AnimationSupported = coder.CodecInfo.SupportsAnimation;
        BitsPerPixel = source.Format.BitsPerPixel;
        Dpi = new Size((float)source.DpiX, (float)source.DpiY);
        HasPalette = ((source.Palette != null) && (source.Palette.Colors.Count > 0)) ? true : false;
        ImageSize = new Size((float)source.Height, (float)source.Width);
        Masks = source.Format.Masks.ToList();
        Palette = source.Palette;
        PixelFormat = source.Format;
        PixelSize = new Size(source.PixelHeight, source.PixelWidth);

        HasThumbnail = coder.Thumbnail != null;
        if (HasThumbnail) {
            Thumbnail = (BitmapImage)coder.Thumbnail.CloneCurrentValue();
        }
    }

    public bool Animated { get; }
    public bool AnimationSupported { get; }
    public int BitsPerPixel { get; }
    public Size Dpi { get; }
    public int Frames { get; }
    public FramesInfo FramesContent => framesInfo;
    public bool HasPalette { get; }
    public bool HasThumbnail { get; }
    public Size ImageSize { get; }
    public string ImageType { get; }
    public bool IsMetadataSupported { get; } = true;
    public List<PixelFormatChannelMask> Masks { get; }
    public MetadataInfo Metadata { get; }
    public BitmapPalette Palette { get; }
    public PixelFormat PixelFormat { get; }
    public Size PixelSize { get; }
    public BitmapImage Thumbnail { get; }

    public enum DeepScanOptions : int
    {
        Default = 0,
        Skip,
        Force
    }

    public enum GrayScaleInfo : int
    {
        None = 0,
        Partial, 
        GrayScale,
        Undefined
    }

    private void FillLongNameTable()
    {
        ImgTypeLongNames = new Dictionary<string, string>
        {
            { "GIF", "Unisis Graphic Interchange Format (GIF)" },
            { "JPG", "Joint Photographics Experts Group JFIF/JPEG" },
            { "PNG", "Portable Network Graphics (PNG)" },
            { "TIFF", "Tagged Image File Format (TIFF)" },
            { "BMP", "Windows Bitmap Format (BMP)" },
            { "WMP", "Windows Media Photo File (WMP)" },
            { "ICO", "Windows Icon (ICO)" }
        };
    }

    public string GetFormatLongName(string format) => ImgTypeLongNames[format];

    public class MetadataInfo {
        public MetadataInfo(BitmapMetadata metadata) {
            ApplicationName = metadata.ApplicationName;
            Author = metadata.Author?.ToList();
            Copyright = metadata.Copyright;
            CameraManufacturer = metadata.CameraManufacturer;
            CameraModel = metadata.CameraModel;
            CameraModel = metadata.Comment;
            DateTaken = metadata.DateTaken;
            Format = metadata.Format;
            Rating = metadata.Rating;
            Subject = metadata.Subject;
            Title = metadata.Title;
        }

        public string ApplicationName { get; }
        public List<string> Author { get; }
        public string Copyright { get; }
        public string CameraManufacturer { get; }
        public string CameraModel { get; }
        public string Comment { get; }
        public string DateTaken { get; }
        public string Format { get; }
        public int Rating { get; }
        public string Subject { get; }
        public string Title { get; }
    }

    public System.Drawing.Bitmap ThumbnailToBitmap()
    {
        if (Thumbnail == null) return null;

        var bitmap = new System.Drawing.Bitmap(Thumbnail.DecodePixelWidth, Thumbnail.DecodePixelHeight);
        var stream = new MemoryStream();
        BitmapEncoder encoder = new BmpBitmapEncoder();
        encoder.Frames.Add(BitmapFrame.Create(Thumbnail));
        encoder.Save(stream);
        return (System.Drawing.Bitmap)System.Drawing.Image.FromStream(stream);
    }

    public void FrameSourceAddRange(BitmapFrame[] bitmapFrames)
    {
        if (bitmapFrames == null) return;

        framesInfo.Frames.AddRange(bitmapFrames.Select(bf => new FramesInfo.Frame() 
        { 
            Palette = bf.Palette,
            FrameSize = new Size(bf.PixelWidth, bf.PixelHeight),
            FrameDpi = new Size(bf.DpiX, bf.DpiY),
            PixelFormat = bf.Format,
            IsGrayScaleFrame = CheckIfGrayScale(bf.Format, bf.Palette, DeepScanOptions.Force),
            IsBlackWhiteFrame = bf.Format == PixelFormats.BlackWhite
        }));

        framesInfo.Frames.Where(f => (!f.IsGrayScaleFrame & !f.IsBlackWhiteFrame)).All(f => f.IsColorFrame);
    }

    public GrayScaleInfo IsGrayScaleFrames()
    {
        if (framesInfo.Frames.Count == 0) return GrayScaleInfo.Undefined;
        if (framesInfo.FramesGrayscaleNumber > 0) {
            return (framesInfo.FramesGrayscaleNumber == framesInfo.FramesTotalNumber)
                    ? GrayScaleInfo.GrayScale
                    : GrayScaleInfo.Partial;
        }
        return GrayScaleInfo.None;
    }

    public bool IsGrayScale(DeepScanOptions DeepScan) => CheckIfGrayScale(PixelFormat, Palette, DeepScan);

    private bool CheckIfGrayScale(PixelFormat pixelFormat, BitmapPalette palette, DeepScanOptions DeepScan)
    {
        if (pixelFormat == PixelFormats.Gray32Float ||
            pixelFormat == PixelFormats.Gray16 ||
            pixelFormat == PixelFormats.Gray8 ||
            pixelFormat == PixelFormats.Gray4 ||
            pixelFormat == PixelFormats.Gray2)
        {
            if (palette == null || (DeepScan != DeepScanOptions.Force)) { return true; }
        }

        if (pixelFormat == PixelFormats.Indexed8 ||
            pixelFormat == PixelFormats.Indexed4 ||
            pixelFormat == PixelFormats.Indexed2)
        {
            DeepScan = (DeepScan != DeepScanOptions.Skip) ? DeepScanOptions.Force : DeepScan;
        }

        if ((DeepScan != DeepScanOptions.Skip) & palette != null)
        {
            List<Color> indexedColors = palette.Colors.ToList();
            return indexedColors.All(rgb => (rgb.R == rgb.G && rgb.G == rgb.B && rgb.B == rgb.R));
        }
        return false;
    }

    public GrayScaleStats GrayScaleSimilarity()
    {
        if (!HasPalette) return null;
        return new GrayScaleStats(framesInfo.Frames);
    }

    public class GrayScaleStats
    {
        public GrayScaleStats(IEnumerable<FramesInfo.Frame> frames) {

            float accumulatorMax = 0F;
            float accumulatorMin = 0F;
            float accumulatorAvg = 0F;
            float[] distance = new float[3];

            Palettes = frames.Count();

            foreach (FramesInfo.Frame frame in frames) {

                foreach (Color pEntry in frame.Palette.Colors) {
                    if (!(pEntry.R == pEntry.G && pEntry.G == pEntry.B && pEntry.B == pEntry.R)) {
                        distance[0] = Math.Abs(pEntry.R - pEntry.G);
                        distance[1] = Math.Abs(pEntry.G - pEntry.B);
                        distance[2] = Math.Abs(pEntry.B - pEntry.R);
                        accumulatorMax += distance.Max();
                        accumulatorMin += distance.Min();
                        accumulatorAvg += distance.Average();
                    }
                }
                int colorEntries = frame.Palette.Colors.Count;

                var distanceAverage = (float)((accumulatorAvg / 2.56) / colorEntries);
                var distanceMax = (float)((accumulatorMax / 2.56) / colorEntries);
                var distanceMin = (float)((accumulatorMin / 2.56) / colorEntries);

                var framestat = new FrameStat(colorEntries, distanceAverage, distanceMax, distanceMin);
                PerFrameValues.Add(framestat);
                accumulatorAvg = 0F;
                accumulatorMax = 0F;
                accumulatorMin = 0F;
            }

            AverageLogDistance = PerFrameValues.Average(avg => avg.DistanceAverage);
            AverageMaxDistance = PerFrameValues.Max(mx => mx.DistanceMax);
            AverageMinDistance = PerFrameValues.Min(mn => mn.DistanceMin);
            GrayScaleAveragePercent = 100F - AverageLogDistance;
            GrayScalePercent = 100F - ((AverageMaxDistance - AverageMinDistance) / 2);
        }

        public float AverageMaxDistance { get; }
        public float AverageMinDistance { get; }
        public float AverageLogDistance { get; }
        public float GrayScalePercent { get; }
        public float GrayScaleAveragePercent { get; }
        public int Palettes { get; }
        public List<FrameStat> PerFrameValues { get; } = new List<FrameStat>();

        public class FrameStat
        {
            public FrameStat(int entries, float average, float max, float min) {
                ColorEntries = entries;
                DistanceAverage = average;
                DistanceMax = max;
                DistanceMin = min;
            }
            public int ColorEntries { get; private set; }
            public float DistanceAverage { get; private set; }
            public float DistanceMax { get; private set; }
            public float DistanceMin { get; private set; }
        }
    }

    public class FramesInfo
    {
        public FramesInfo() => Frames = new List<Frame>();

        public int FramesTotalNumber
        {
            get => (Frames != null) ? Frames.Count() : 0;
            private set { }
        }

        public int FramesColorNumber
        {
            get => (Frames != null) ? Frames.Where(f => f.IsColorFrame == true).Count() : 0;
            private set { }
        }
        public int FramesGrayscaleNumber
        {
            get => (Frames != null) ? Frames.Where(f => f.IsGrayScaleFrame == true).Count() : 0;
            private set { }
        }

        public int FramesBlackWhiteNumber
        {
            get => (Frames != null) ? Frames.Where(f => f.IsBlackWhiteFrame == true).Count() : 0;
            private set { }
        }

        public List<Frame> Frames { get; private set; }

        public class Frame
        {
            public BitmapPalette Palette { get; set; }
            public Size FrameSize { get; set; }
            public Size FrameDpi { get; set; }
            public PixelFormat PixelFormat { get; set; }
            public bool IsColorFrame { get; set; }
            public bool IsGrayScaleFrame { get; set; }
            public bool IsBlackWhiteFrame { get; set; }
        }
    }
}

public static ImagingBitmapInfo BitmapPixelFormat(string FileName)
{
    using (var stream = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.None)) {
        return BitmapPixelFormat(stream);
    }
}

public static ImagingBitmapInfo BitmapPixelFormat(FileStream stream) => new ImagingBitmapInfo(stream);
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • How to detect JPG with 256 colors (grayscale)? Both PresentationCore.dll and System.Drawing.BItmap wrongly detect it as 24 bpp. – user3625699 Sep 04 '21 at 22:55
  • @user3625699 If the JPEG image is truly grayscale, then `BitmapInfo.IsGrayScaleFrames()` returns `GrayScaleInfo.GrayScale`, `BitmapInfo.IsGrayScale(DeepScanOptions.Default)` returns `true` and `BitmapInfo.PixelFormat` is set to `Gray8`. No doubt. If you have a sample Image that fails the GrayScale test, then post this sample image, I'll give it a look. Anyway, there's no chance that a `Gray8` image tests as a 24bpp image. Note that if you don't transform the Image to indexed before using a tool to convert to Gray Scale (as in PhotoShop) you don't get a GrayScale image, it's still TrueColor. – Jimi Sep 05 '21 at 03:48