ImageCompare.cs
- //
- // This code is part of Document Solutions for Imaging demos.
- // Copyright (c) MESCIUS inc. All rights reserved.
- //
- using System;
- using System.IO;
- using System.Drawing;
- using System.Collections.Generic;
- using System.Linq;
- using GrapeCity.Documents.Drawing;
- using GrapeCity.Documents.Text;
- using GrapeCity.Documents.Imaging;
- using GCTEXT = GrapeCity.Documents.Text;
- using GCDRAW = GrapeCity.Documents.Drawing;
-
- namespace DsImagingWeb.Demos
- {
- // This sample implements pixel by pixel image comparison.
- //
- // CIE L*a*b* color space is used for color comparison,
- // XYZ color space is used to convert colors between RGB and Lab.
- // The 'difference' image is a low-contrast grayscale representation of the first image,
- // overlaid with magenta colored pixels that are different, magenta intensity proportional
- // to the difference between the two original pixels.
- // While all output formats are supported by this sample, TIFF is preferable
- // as the 2nd and 3rd pages of the generated TIFF show the two original images.
- //
- // Color math taken from EasyRGB.
- // Color comparison formula from Identifying Color Differences Using L*a*b* or L*C*H* Coordinates.
- public class ImageCompare
- {
- public Stream GenerateImageStream(string targetMime, Size pixelSize, float dpi, bool opaque, string[] sampleParams = null)
- {
- sampleParams ??= GetSampleParamsList()[0];
-
- var path1 = sampleParams[3];
- var path2 = sampleParams[4];
- // Comparison fuzziness (from 0 to MaxDelta):
- int fuzz = sampleParams == null ? 12 : int.Parse(sampleParams[5]);
- // Different pixels will be this color:
- uint highlight = (uint)Color.Magenta.ToArgb();
- // Fill for empty areas not occupied by any image:
- uint fill = (uint)Color.White.ToArgb();
- // Q is used to enhance the intensity of the differences,
- // we do it in inverse proportion to the fuzz (0 fuzz will make
- // even the smallest differences pop out):
- int Q = (int)(255d / (fuzz + 1));
- // Reference image transparency:
- const uint alphaBack = 55 << 24;
-
- var bmp1 = new GcBitmap(path1);
- var bmp2 = new GcBitmap(path2);
-
- // If images are too large, resize to fit target width/height:
- double z1 = 1, z2 = 1;
- if (bmp1.PixelWidth > pixelSize.Width)
- z1 = (double)pixelSize.Width / bmp1.PixelWidth;
- if (bmp1.PixelHeight > pixelSize.Height)
- z1 = Math.Min(z1, (double)pixelSize.Height / bmp1.PixelHeight);
- if (bmp2.PixelWidth > pixelSize.Width)
- z2 = (double)pixelSize.Width / bmp2.PixelWidth;
- if (bmp2.PixelHeight > pixelSize.Height)
- z2 = Math.Min(z2, (double)pixelSize.Height / bmp2.PixelHeight);
-
- // Resize images to same (minimum) widths:
- if (bmp1.PixelWidth * z1 > bmp2.PixelWidth * z2)
- z1 = (double)bmp2.PixelWidth / bmp1.PixelWidth;
- else if (bmp2.PixelWidth * z2 > bmp1.PixelWidth * z1)
- z2 = (double)bmp1.PixelWidth / bmp2.PixelWidth;
-
- if (z1 < 1)
- {
- var t = bmp1.Resize((int)Math.Round(bmp1.PixelWidth * z1), (int)Math.Round(bmp1.PixelHeight * z1),
- z1 < 0.5 ? InterpolationMode.Downscale : InterpolationMode.Cubic);
- bmp1.Dispose();
- bmp1 = t;
- }
- if (z2 < 1)
- {
- var t = bmp2.Resize((int)Math.Round(bmp2.PixelWidth * z2), (int)Math.Round(bmp2.PixelHeight * z2),
- z2 < 0.5 ? InterpolationMode.Downscale : InterpolationMode.Cubic);
- bmp2.Dispose();
- bmp2 = t;
- }
-
- // Color difference math from
- // https://sensing.konicaminolta.us/us/blog/identifying-color-differences-using-l-a-b-or-l-c-h-coordinates/
-
- // We assume that for RGB space, L*a*b* ranges are
- // L* from 0 to 100, a* and b* from -128 to + 127, see
- // http://www.colourphil.co.uk/lab_lch_colour_space.shtml
- // So max possible difference between any two colors in RGB space would be:
- // MaxDelta = 374.23254802328461
- var MaxDelta = LabDistance((0, -128, -128), (100, 127, 127));
-
- using (var bmp = new GcBitmap(pixelSize.Width, pixelSize.Height, false, dpi, dpi))
- using (var g = bmp.CreateGraphics(Color.FromArgb((int)fill)))
- {
- // Total number of different pixels found:
- int differences = 0;
-
- var w = bmp1.PixelWidth;
- var h = Math.Min(bmp1.PixelHeight, bmp2.PixelHeight);
-
- for (int i = 0; i < w; ++i)
- {
- for (int j = 0; j < h; ++j)
- {
- var px1 = bmp1[i, j];
- var px2 = bmp2[i, j];
-
- // Convert RGB to CIE L*a*b* space to calc the difference:
- var (L1, a1, b1) = ColorXYZ.FromRGB((int)px1).ToCIELab();
- if (px1 == px2)
- {
- // To speed things up, we skip 2nd color calculations for identical pixels:
- // Equal pixels are drawn in semi-transparent gray to give a reference to the differences:
- uint gray = (uint)Math.Round((L1 * 255d) / 100d);
- bmp[i, j] = alphaBack | (gray << 16) | (gray << 8) | gray;
- }
- else
- {
- // Otherwise, calc the distance between the two colors:
- var (L2, a2, b2) = ColorXYZ.FromRGB((int)px2).ToCIELab();
- var delta = LabDistance((L1, a1, b1), (L2, a2, b2));
- if (delta > fuzz)
- {
- uint alpha = (uint)Math.Min(255, ((255 * delta) / MaxDelta) * Q);
- bmp[i, j] = (alpha << 24) | highlight;
- ++differences;
- }
- else
- {
- // See equal pixels comment above:
- uint gray = (uint)Math.Round((L1 * 255d) / 100d);
- bmp[i, j] = alphaBack | (gray << 16) | (gray << 8) | gray;
- }
- }
- }
- }
-
- // For consistency, convert the difference to opaque:
- bmp.ConvertToOpaque(Color.White);
-
- // Text layout for info texts:
- var tl = new TextLayout(g.Resolution) { TextAlignment = TextAlignment.Trailing };
- tl.DefaultFormat.Font = GCTEXT.Font.FromFile(Path.Combine("Resources", "Fonts", "arial.ttf"));
- tl.DefaultFormat.FontSize = 12;
- tl.DefaultFormat.ForeColor = Color.Blue;
- tl.MaxWidth = g.Width;
- tl.MarginAll = 4;
-
- // Save to target format:
- var ms = new MemoryStream();
- if (targetMime == Common.Util.MimeTypes.TIFF)
- {
- // For TIFF, render 3 images (diff and 2 sources) on separate pages:
- tl.Append($"Found {differences} different pixels (fuzz {fuzz}).");
- g.DrawTextLayout(tl, PointF.Empty);
- using (var tw = new GcTiffWriter(ms))
- {
- tw.AppendFrame(bmp);
- bmp1.ConvertToOpaque(Color.White);
- using (var g1 = bmp1.CreateGraphics())
- {
- tl.Clear();
- tl.MaxWidth = g1.Width;
- tl.DefaultFormat.BackColor = Color.LightYellow;
- tl.Append(Path.GetFileName(path1));
- g1.DrawTextLayout(tl, PointF.Empty);
- }
- tw.AppendFrame(bmp1);
- bmp2.ConvertToOpaque(Color.White);
- using (var g2 = bmp2.CreateGraphics())
- {
- tl.Clear();
- tl.MaxWidth = g2.Width;
- tl.Append(Path.GetFileName(path2));
- g2.DrawTextLayout(tl, PointF.Empty);
- }
- tw.AppendFrame(bmp2);
- }
- bmp.SaveAsTiff(ms);
- }
- else
- {
- // For other formats, tile the diff and sources:
- using var tbmp = TileImages(pixelSize, bmp, new Size(w, h), bmp1, bmp2, dpi, tl, Path.GetFileName(path1), Path.GetFileName(path2));
- using (var tg = tbmp.CreateGraphics())
- {
- tl.TextAlignment = TextAlignment.Trailing;
- tl.MaxWidth = tg.Width;
- tl.Clear();
- tl.Append($"Found {differences} different pixels (fuzz {fuzz}).");
- tg.DrawTextLayout(tl, PointF.Empty);
- }
- switch (targetMime)
- {
- case Common.Util.MimeTypes.JPEG:
- tbmp.SaveAsJpeg(ms);
- break;
- case Common.Util.MimeTypes.PNG:
- tbmp.SaveAsPng(ms);
- break;
- case Common.Util.MimeTypes.BMP:
- tbmp.SaveAsBmp(ms);
- break;
- case Common.Util.MimeTypes.GIF:
- tbmp.SaveAsGif(ms);
- break;
- case Common.Util.MimeTypes.WEBP:
- bmp.SaveAsWebp(ms);
- break;
- default:
- throw new Exception($"Encoding {targetMime} is not supported.");
- }
- }
- bmp1.Dispose();
- bmp2.Dispose();
-
- ms.Seek(0, SeekOrigin.Begin);
- return ms;
- }
- }
-
- private static GcBitmap TileImages(Size targetSize, GcBitmap diff, Size diffSize, GcBitmap bmp1, GcBitmap bmp2, float dpi, TextLayout tl, string name1, string name2)
- {
- Size tSize = new Size(targetSize.Width / 2 - 1, targetSize.Width / 2 - 1);
-
- var bmp = new GcBitmap(targetSize.Width, targetSize.Height, true, dpi, dpi);
- using (var diffClip = diff.Clip(new Rectangle(Point.Empty, diffSize)))
- {
- bmp1.ConvertToOpaque(Color.White);
- bmp2.ConvertToOpaque(Color.White);
-
- var ts0 = FitSize(diffClip, tSize);
- var ts1 = FitSize(bmp1, tSize);
- var ts2 = FitSize(bmp2, tSize);
- using (var g = bmp.CreateGraphics(Color.White))
- {
- g.DrawLine(0, ts0.Height, g.Width, ts0.Height, Color.Yellow);
- g.DrawLine(ts1.Width + 1, diffClip.Height, ts1.Width + 1, ts0.Height + ts1.Height, Color.Yellow);
- }
-
- int x;
- if (ts0.IsEmpty)
- {
- x = ts1.Width - diffClip.PixelWidth / 2;
- bmp.BitBlt(diffClip, x, 0);
- }
- else
- {
- x = ts1.Width - ts0.Width / 2;
- using (var tbmp = diffClip.Resize(ts0.Width, ts0.Height, InterpolationMode.Cubic))
- bmp.BitBlt(tbmp, x, 0);
- }
- if (ts1.IsEmpty)
- bmp.BitBlt(bmp1, 0, ts0.Height + 1);
- else
- using (var tbmp = bmp1.Resize(ts1.Width, ts1.Height, InterpolationMode.Cubic))
- bmp.BitBlt(tbmp, 0, ts0.Height + 1);
- if (ts2.IsEmpty)
- bmp.BitBlt(bmp2, ts1.Width + 1, ts0.Height + 1);
- else
- using (var tbmp = bmp2.Resize(ts2.Width, ts2.Height, InterpolationMode.Cubic))
- bmp.BitBlt(tbmp, ts1.Width + 1, ts0.Height + 1);
-
- using (var g = bmp.CreateGraphics())
- {
- tl.TextAlignment = TextAlignment.Leading;
- tl.DefaultFormat.BackColor = Color.LightYellow;
- tl.Clear();
- tl.MaxWidth = ts0.Width;
- tl.Append("Difference");
- g.DrawTextLayout(tl, new PointF(x, 0));
- tl.Clear();
- tl.MaxWidth = ts1.Width;
- tl.Append(name1);
- g.DrawTextLayout(tl, new PointF(0, ts0.Height + 1));
- tl.Clear();
- tl.MaxWidth = ts2.Width;
- tl.Append(name2);
- g.DrawTextLayout(tl, new PointF(ts1.Width + 1, ts0.Height + 1));
- }
- }
- return bmp;
- }
-
- private static Size FitSize(GcBitmap bmp, Size size)
- {
- double z = 1;
- if (bmp.PixelWidth > size.Width)
- z = (double)size.Width / bmp.PixelWidth;
- if (bmp.PixelHeight > size.Height)
- z = Math.Min(z, (double)size.Height / bmp.PixelHeight);
- if (z < 1)
- return new Size((int)Math.Round(bmp.PixelWidth * z), (int)Math.Round(bmp.PixelHeight * z));
- else
- return Size.Empty; // indicates that no resizing is needed
- }
-
- public static double LabDistance((double L, double a, double b) lab1, (double L, double a, double b) lab2)
- {
- var dL = lab1.L - lab2.L;
- var da = lab1.a - lab2.a;
- var db = lab1.b - lab2.b;
- return Math.Sqrt(dL * dL + da * da + db * db);
- }
-
- public string DefaultMime { get => Common.Util.MimeTypes.TIFF; }
-
- public static List<string[]> GetSampleParamsList()
- {
- return new List<string[]>()
- {
- // Strings are name, description, info. Rest are arbitrary strings, in this sample these are:
- // - first file to compare;
- // - second file to compare;
- // - compare fuzz (integer):
- new string[] { "Find Differences", "Compare two similar images with few minor differences (fuzz 12)", null,
- Path.Combine("Resources", "Images", "newfoundland.jpg"), Path.Combine("Resources", "ImageCompare", "newfoundland-mod.jpg"), "12" },
- new string[] { "Invisible Text", "Compare an image with same image that has a semi-transparent text overlay (fuzz 0)", null,
- Path.Combine("Resources", "ImageCompare", "seville.png"), Path.Combine("Resources", "ImageCompare", "seville-text.png"), "0" },
- new string[] { "PNG vs JPEG", "Compare a PNG image with the same image saved as a 75% quality JPEG (fuzz 6)", null,
- Path.Combine("Resources", "ImageCompare", "toronto-lights.png"), Path.Combine("Resources", "ImageCompare", "toronto-lights-75.jpg"), "6" },
- new string[] { "Font Hinting", "Compare text rendered with TrueType font hinting on and off (fuzz 1)", null,
- Path.Combine("Resources", "ImageCompare", "TrueTypeHinting-on.png"), Path.Combine("Resources", "ImageCompare", "TrueTypeHinting-off.png"), "1" },
- };
- }
-
- // An XYZ color type, based on color math from
- // https://www.easyrgb.com/en/math.php
- public class ColorXYZ : IEquatable<ColorXYZ>
- {
- private readonly double _x, _y, _z;
-
- // D65 CIE 1964 ref values (sRGB, AdobeRGB):
- const double ReferenceX = 94.811;
- const double ReferenceY = 100.000;
- const double ReferenceZ = 107.304;
-
- public double X => _x;
- public double Y => _y;
- public double Z => _z;
-
- private ColorXYZ(double x, double y, double z)
- {
- _x = x;
- _y = y;
- _z = z;
- }
-
- public bool Equals(ColorXYZ other)
- {
- if ((object)other == null)
- return false;
- return _x == other._x && _y == other._y && _z == other._z;
-
- /*
- return Same(_x, other._x) && Same(_y, other._y) && Same(_z, other._z);
- bool Same(double a, double b)
- {
- return Math.Abs(a - b) <= a / 10000;
- }
- */
- }
-
- public override bool Equals(object obj)
- {
- return Equals(obj as ColorXYZ);
- }
-
- public static bool operator ==(ColorXYZ obj1, ColorXYZ obj2)
- {
- return (object)obj1 != null && obj1.Equals(obj2);
- }
-
- public static bool operator !=(ColorXYZ obj1, ColorXYZ obj2)
- {
- return (object)obj1 == null || !obj1.Equals(obj2);
- }
-
- public override int GetHashCode()
- {
- return _x.GetHashCode() ^ _y.GetHashCode() ^ _z.GetHashCode();
- }
-
- public static ColorXYZ FromXYZ(double x, double y, double z)
- {
- return new ColorXYZ(x, y, z);
- }
-
- public static ColorXYZ FromRGB(Color rgb)
- {
- return FromRGB(rgb.R, rgb.G, rgb.B);
- }
-
- public static ColorXYZ FromRGB(int rgb)
- {
- return FromRGB((rgb & 0x00FF0000) >> 16, (rgb & 0x0000FF00) >> 8, rgb & 0x000000FF);
- }
-
- public static ColorXYZ FromRGB(int r, int g, int b)
- {
- double var_R = (r / 255d);
- double var_G = (g / 255d);
- double var_B = (b / 255d);
-
- if (var_R > 0.04045)
- var_R = Math.Pow((var_R + 0.055) / 1.055, 2.4);
- else
- var_R = var_R / 12.92;
- if (var_G > 0.04045)
- var_G = Math.Pow((var_G + 0.055) / 1.055, 2.4);
- else
- var_G /= 12.92;
- if (var_B > 0.04045)
- var_B = Math.Pow((var_B + 0.055) / 1.055, 2.4);
- else
- var_B /= 12.92;
-
- var_R *= 100;
- var_G *= 100;
- var_B *= 100;
-
- return new ColorXYZ(
- var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805,
- var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722,
- var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
- );
- }
-
- public static ColorXYZ FromCIELab((double L, double a, double b) lab)
- {
- return FromCIELab(lab.L, lab.a, lab.b);
- }
-
- public static ColorXYZ FromCIELab(double L, double a, double b)
- {
- double var_Y = (L + 16) / 116d;
- double var_X = a / 500d + var_Y;
- double var_Z = var_Y - b / 200d;
-
- if (Math.Pow(var_Y, 3) > 0.008856)
- var_Y = Math.Pow(var_Y, 3);
- else
- var_Y = (var_Y - 16d / 116d) / 7.787;
- if (Math.Pow(var_X, 3) > 0.008856)
- var_X = Math.Pow(var_X, 3);
- else
- var_X = (var_X - 16d / 116d) / 7.787;
- if (Math.Pow(var_Z, 3) > 0.008856)
- var_Z = Math.Pow(var_Z, 3);
- else
- var_Z = (var_Z - 16d / 116d) / 7.787;
-
- return new ColorXYZ(
- var_X * ReferenceX,
- var_Y * ReferenceY,
- var_Z * ReferenceZ
- );
- }
-
- public Color ToRGB()
- {
- double var_X = _x / 100;
- double var_Y = _y / 100;
- double var_Z = _z / 100;
-
- double var_R = var_X * 3.2406 + var_Y * -1.5372 + var_Z * -0.4986;
- double var_G = var_X * -0.9689 + var_Y * 1.8758 + var_Z * 0.0415;
- double var_B = var_X * 0.0557 + var_Y * -0.2040 + var_Z * 1.0570;
-
- if (var_R > 0.0031308)
- var_R = 1.055 * Math.Pow(var_R, (1 / 2.4)) - 0.055;
- else
- var_R = 12.92 * var_R;
- if (var_G > 0.0031308)
- var_G = 1.055 * Math.Pow(var_G, (1 / 2.4)) - 0.055;
- else
- var_G = 12.92 * var_G;
- if (var_B > 0.0031308)
- var_B = 1.055 * Math.Pow(var_B, (1 / 2.4)) - 0.055;
- else
- var_B = 12.92 * var_B;
-
- return Color.FromArgb(
- (int)Math.Round(var_R * 255),
- (int)Math.Round(var_G * 255),
- (int)Math.Round(var_B * 255)
- );
- }
-
- public (double L, double a, double b) ToCIELab()
- {
- double var_X = _x / ReferenceX;
- double var_Y = _y / ReferenceY;
- double var_Z = _z / ReferenceZ;
-
- if (var_X > 0.008856)
- var_X = Math.Pow(var_X, 1 / 3d);
- else
- var_X = (7.787 * var_X) + (16d / 116d);
- if (var_Y > 0.008856)
- var_Y = Math.Pow(var_Y, 1 / 3d);
- else
- var_Y = (7.787 * var_Y) + (16d / 116d);
- if (var_Z > 0.008856)
- var_Z = Math.Pow(var_Z, 1 / 3d);
- else
- var_Z = (7.787 * var_Z) + (16d / 116d);
-
- return (
- (116 * var_Y) - 16,
- 500 * (var_X - var_Y),
- 200 * (var_Y - var_Z)
- );
- }
- }
- }
- }
-