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 GrapeCity.Documents.Drawing;using GrapeCity.Documents.Text;using GrapeCity.Documents.Imaging;using GCTEXT = GrapeCity.Documents.Text;
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 = g.CreateTextLayout(); tl.TextAlignment = TextAlignment.Trailing; tl.DefaultFormat.Font = GCTEXT.Font.FromFile(Path.Combine("Resources", "Fonts", "NotoSans-Regular.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) ); } } }}