ImageCompare.cs
  1. //
  2. // This code is part of Document Solutions for Imaging demos.
  3. // Copyright (c) MESCIUS inc. All rights reserved.
  4. //
  5. using System;
  6. using System.IO;
  7. using System.Drawing;
  8. using System.Collections.Generic;
  9. using System.Linq;
  10. using GrapeCity.Documents.Drawing;
  11. using GrapeCity.Documents.Text;
  12. using GrapeCity.Documents.Imaging;
  13. using GCTEXT = GrapeCity.Documents.Text;
  14. using GCDRAW = GrapeCity.Documents.Drawing;
  15.  
  16. namespace DsImagingWeb.Demos
  17. {
  18. // This sample implements pixel by pixel image comparison.
  19. //
  20. // CIE L*a*b* color space is used for color comparison,
  21. // XYZ color space is used to convert colors between RGB and Lab.
  22. // The 'difference' image is a low-contrast grayscale representation of the first image,
  23. // overlaid with magenta colored pixels that are different, magenta intensity proportional
  24. // to the difference between the two original pixels.
  25. // While all output formats are supported by this sample, TIFF is preferable
  26. // as the 2nd and 3rd pages of the generated TIFF show the two original images.
  27. //
  28. // Color math taken from EasyRGB.
  29. // Color comparison formula from Identifying Color Differences Using L*a*b* or L*C*H* Coordinates.
  30. public class ImageCompare
  31. {
  32. public Stream GenerateImageStream(string targetMime, Size pixelSize, float dpi, bool opaque, string[] sampleParams = null)
  33. {
  34. sampleParams ??= GetSampleParamsList()[0];
  35.  
  36. var path1 = sampleParams[3];
  37. var path2 = sampleParams[4];
  38. // Comparison fuzziness (from 0 to MaxDelta):
  39. int fuzz = sampleParams == null ? 12 : int.Parse(sampleParams[5]);
  40. // Different pixels will be this color:
  41. uint highlight = (uint)Color.Magenta.ToArgb();
  42. // Fill for empty areas not occupied by any image:
  43. uint fill = (uint)Color.White.ToArgb();
  44. // Q is used to enhance the intensity of the differences,
  45. // we do it in inverse proportion to the fuzz (0 fuzz will make
  46. // even the smallest differences pop out):
  47. int Q = (int)(255d / (fuzz + 1));
  48. // Reference image transparency:
  49. const uint alphaBack = 55 << 24;
  50.  
  51. var bmp1 = new GcBitmap(path1);
  52. var bmp2 = new GcBitmap(path2);
  53.  
  54. // If images are too large, resize to fit target width/height:
  55. double z1 = 1, z2 = 1;
  56. if (bmp1.PixelWidth > pixelSize.Width)
  57. z1 = (double)pixelSize.Width / bmp1.PixelWidth;
  58. if (bmp1.PixelHeight > pixelSize.Height)
  59. z1 = Math.Min(z1, (double)pixelSize.Height / bmp1.PixelHeight);
  60. if (bmp2.PixelWidth > pixelSize.Width)
  61. z2 = (double)pixelSize.Width / bmp2.PixelWidth;
  62. if (bmp2.PixelHeight > pixelSize.Height)
  63. z2 = Math.Min(z2, (double)pixelSize.Height / bmp2.PixelHeight);
  64.  
  65. // Resize images to same (minimum) widths:
  66. if (bmp1.PixelWidth * z1 > bmp2.PixelWidth * z2)
  67. z1 = (double)bmp2.PixelWidth / bmp1.PixelWidth;
  68. else if (bmp2.PixelWidth * z2 > bmp1.PixelWidth * z1)
  69. z2 = (double)bmp1.PixelWidth / bmp2.PixelWidth;
  70.  
  71. if (z1 < 1)
  72. {
  73. var t = bmp1.Resize((int)Math.Round(bmp1.PixelWidth * z1), (int)Math.Round(bmp1.PixelHeight * z1),
  74. z1 < 0.5 ? InterpolationMode.Downscale : InterpolationMode.Cubic);
  75. bmp1.Dispose();
  76. bmp1 = t;
  77. }
  78. if (z2 < 1)
  79. {
  80. var t = bmp2.Resize((int)Math.Round(bmp2.PixelWidth * z2), (int)Math.Round(bmp2.PixelHeight * z2),
  81. z2 < 0.5 ? InterpolationMode.Downscale : InterpolationMode.Cubic);
  82. bmp2.Dispose();
  83. bmp2 = t;
  84. }
  85.  
  86. // Color difference math from
  87. // https://sensing.konicaminolta.us/us/blog/identifying-color-differences-using-l-a-b-or-l-c-h-coordinates/
  88.  
  89. // We assume that for RGB space, L*a*b* ranges are
  90. // L* from 0 to 100, a* and b* from -128 to + 127, see
  91. // http://www.colourphil.co.uk/lab_lch_colour_space.shtml
  92. // So max possible difference between any two colors in RGB space would be:
  93. // MaxDelta = 374.23254802328461
  94. var MaxDelta = LabDistance((0, -128, -128), (100, 127, 127));
  95.  
  96. using (var bmp = new GcBitmap(pixelSize.Width, pixelSize.Height, false, dpi, dpi))
  97. using (var g = bmp.CreateGraphics(Color.FromArgb((int)fill)))
  98. {
  99. // Total number of different pixels found:
  100. int differences = 0;
  101.  
  102. var w = bmp1.PixelWidth;
  103. var h = Math.Min(bmp1.PixelHeight, bmp2.PixelHeight);
  104.  
  105. for (int i = 0; i < w; ++i)
  106. {
  107. for (int j = 0; j < h; ++j)
  108. {
  109. var px1 = bmp1[i, j];
  110. var px2 = bmp2[i, j];
  111.  
  112. // Convert RGB to CIE L*a*b* space to calc the difference:
  113. var (L1, a1, b1) = ColorXYZ.FromRGB((int)px1).ToCIELab();
  114. if (px1 == px2)
  115. {
  116. // To speed things up, we skip 2nd color calculations for identical pixels:
  117. // Equal pixels are drawn in semi-transparent gray to give a reference to the differences:
  118. uint gray = (uint)Math.Round((L1 * 255d) / 100d);
  119. bmp[i, j] = alphaBack | (gray << 16) | (gray << 8) | gray;
  120. }
  121. else
  122. {
  123. // Otherwise, calc the distance between the two colors:
  124. var (L2, a2, b2) = ColorXYZ.FromRGB((int)px2).ToCIELab();
  125. var delta = LabDistance((L1, a1, b1), (L2, a2, b2));
  126. if (delta > fuzz)
  127. {
  128. uint alpha = (uint)Math.Min(255, ((255 * delta) / MaxDelta) * Q);
  129. bmp[i, j] = (alpha << 24) | highlight;
  130. ++differences;
  131. }
  132. else
  133. {
  134. // See equal pixels comment above:
  135. uint gray = (uint)Math.Round((L1 * 255d) / 100d);
  136. bmp[i, j] = alphaBack | (gray << 16) | (gray << 8) | gray;
  137. }
  138. }
  139. }
  140. }
  141.  
  142. // For consistency, convert the difference to opaque:
  143. bmp.ConvertToOpaque(Color.White);
  144.  
  145. // Text layout for info texts:
  146. var tl = new TextLayout(g.Resolution) { TextAlignment = TextAlignment.Trailing };
  147. tl.DefaultFormat.Font = GCTEXT.Font.FromFile(Path.Combine("Resources", "Fonts", "arial.ttf"));
  148. tl.DefaultFormat.FontSize = 12;
  149. tl.DefaultFormat.ForeColor = Color.Blue;
  150. tl.MaxWidth = g.Width;
  151. tl.MarginAll = 4;
  152.  
  153. // Save to target format:
  154. var ms = new MemoryStream();
  155. if (targetMime == Common.Util.MimeTypes.TIFF)
  156. {
  157. // For TIFF, render 3 images (diff and 2 sources) on separate pages:
  158. tl.Append($"Found {differences} different pixels (fuzz {fuzz}).");
  159. g.DrawTextLayout(tl, PointF.Empty);
  160. using (var tw = new GcTiffWriter(ms))
  161. {
  162. tw.AppendFrame(bmp);
  163. bmp1.ConvertToOpaque(Color.White);
  164. using (var g1 = bmp1.CreateGraphics())
  165. {
  166. tl.Clear();
  167. tl.MaxWidth = g1.Width;
  168. tl.DefaultFormat.BackColor = Color.LightYellow;
  169. tl.Append(Path.GetFileName(path1));
  170. g1.DrawTextLayout(tl, PointF.Empty);
  171. }
  172. tw.AppendFrame(bmp1);
  173. bmp2.ConvertToOpaque(Color.White);
  174. using (var g2 = bmp2.CreateGraphics())
  175. {
  176. tl.Clear();
  177. tl.MaxWidth = g2.Width;
  178. tl.Append(Path.GetFileName(path2));
  179. g2.DrawTextLayout(tl, PointF.Empty);
  180. }
  181. tw.AppendFrame(bmp2);
  182. }
  183. bmp.SaveAsTiff(ms);
  184. }
  185. else
  186. {
  187. // For other formats, tile the diff and sources:
  188. using var tbmp = TileImages(pixelSize, bmp, new Size(w, h), bmp1, bmp2, dpi, tl, Path.GetFileName(path1), Path.GetFileName(path2));
  189. using (var tg = tbmp.CreateGraphics())
  190. {
  191. tl.TextAlignment = TextAlignment.Trailing;
  192. tl.MaxWidth = tg.Width;
  193. tl.Clear();
  194. tl.Append($"Found {differences} different pixels (fuzz {fuzz}).");
  195. tg.DrawTextLayout(tl, PointF.Empty);
  196. }
  197. switch (targetMime)
  198. {
  199. case Common.Util.MimeTypes.JPEG:
  200. tbmp.SaveAsJpeg(ms);
  201. break;
  202. case Common.Util.MimeTypes.PNG:
  203. tbmp.SaveAsPng(ms);
  204. break;
  205. case Common.Util.MimeTypes.BMP:
  206. tbmp.SaveAsBmp(ms);
  207. break;
  208. case Common.Util.MimeTypes.GIF:
  209. tbmp.SaveAsGif(ms);
  210. break;
  211. case Common.Util.MimeTypes.WEBP:
  212. bmp.SaveAsWebp(ms);
  213. break;
  214. default:
  215. throw new Exception($"Encoding {targetMime} is not supported.");
  216. }
  217. }
  218. bmp1.Dispose();
  219. bmp2.Dispose();
  220.  
  221. ms.Seek(0, SeekOrigin.Begin);
  222. return ms;
  223. }
  224. }
  225.  
  226. private static GcBitmap TileImages(Size targetSize, GcBitmap diff, Size diffSize, GcBitmap bmp1, GcBitmap bmp2, float dpi, TextLayout tl, string name1, string name2)
  227. {
  228. Size tSize = new Size(targetSize.Width / 2 - 1, targetSize.Width / 2 - 1);
  229.  
  230. var bmp = new GcBitmap(targetSize.Width, targetSize.Height, true, dpi, dpi);
  231. using (var diffClip = diff.Clip(new Rectangle(Point.Empty, diffSize)))
  232. {
  233. bmp1.ConvertToOpaque(Color.White);
  234. bmp2.ConvertToOpaque(Color.White);
  235.  
  236. var ts0 = FitSize(diffClip, tSize);
  237. var ts1 = FitSize(bmp1, tSize);
  238. var ts2 = FitSize(bmp2, tSize);
  239. using (var g = bmp.CreateGraphics(Color.White))
  240. {
  241. g.DrawLine(0, ts0.Height, g.Width, ts0.Height, Color.Yellow);
  242. g.DrawLine(ts1.Width + 1, diffClip.Height, ts1.Width + 1, ts0.Height + ts1.Height, Color.Yellow);
  243. }
  244.  
  245. int x;
  246. if (ts0.IsEmpty)
  247. {
  248. x = ts1.Width - diffClip.PixelWidth / 2;
  249. bmp.BitBlt(diffClip, x, 0);
  250. }
  251. else
  252. {
  253. x = ts1.Width - ts0.Width / 2;
  254. using (var tbmp = diffClip.Resize(ts0.Width, ts0.Height, InterpolationMode.Cubic))
  255. bmp.BitBlt(tbmp, x, 0);
  256. }
  257. if (ts1.IsEmpty)
  258. bmp.BitBlt(bmp1, 0, ts0.Height + 1);
  259. else
  260. using (var tbmp = bmp1.Resize(ts1.Width, ts1.Height, InterpolationMode.Cubic))
  261. bmp.BitBlt(tbmp, 0, ts0.Height + 1);
  262. if (ts2.IsEmpty)
  263. bmp.BitBlt(bmp2, ts1.Width + 1, ts0.Height + 1);
  264. else
  265. using (var tbmp = bmp2.Resize(ts2.Width, ts2.Height, InterpolationMode.Cubic))
  266. bmp.BitBlt(tbmp, ts1.Width + 1, ts0.Height + 1);
  267.  
  268. using (var g = bmp.CreateGraphics())
  269. {
  270. tl.TextAlignment = TextAlignment.Leading;
  271. tl.DefaultFormat.BackColor = Color.LightYellow;
  272. tl.Clear();
  273. tl.MaxWidth = ts0.Width;
  274. tl.Append("Difference");
  275. g.DrawTextLayout(tl, new PointF(x, 0));
  276. tl.Clear();
  277. tl.MaxWidth = ts1.Width;
  278. tl.Append(name1);
  279. g.DrawTextLayout(tl, new PointF(0, ts0.Height + 1));
  280. tl.Clear();
  281. tl.MaxWidth = ts2.Width;
  282. tl.Append(name2);
  283. g.DrawTextLayout(tl, new PointF(ts1.Width + 1, ts0.Height + 1));
  284. }
  285. }
  286. return bmp;
  287. }
  288.  
  289. private static Size FitSize(GcBitmap bmp, Size size)
  290. {
  291. double z = 1;
  292. if (bmp.PixelWidth > size.Width)
  293. z = (double)size.Width / bmp.PixelWidth;
  294. if (bmp.PixelHeight > size.Height)
  295. z = Math.Min(z, (double)size.Height / bmp.PixelHeight);
  296. if (z < 1)
  297. return new Size((int)Math.Round(bmp.PixelWidth * z), (int)Math.Round(bmp.PixelHeight * z));
  298. else
  299. return Size.Empty; // indicates that no resizing is needed
  300. }
  301.  
  302. public static double LabDistance((double L, double a, double b) lab1, (double L, double a, double b) lab2)
  303. {
  304. var dL = lab1.L - lab2.L;
  305. var da = lab1.a - lab2.a;
  306. var db = lab1.b - lab2.b;
  307. return Math.Sqrt(dL * dL + da * da + db * db);
  308. }
  309.  
  310. public string DefaultMime { get => Common.Util.MimeTypes.TIFF; }
  311.  
  312. public static List<string[]> GetSampleParamsList()
  313. {
  314. return new List<string[]>()
  315. {
  316. // Strings are name, description, info. Rest are arbitrary strings, in this sample these are:
  317. // - first file to compare;
  318. // - second file to compare;
  319. // - compare fuzz (integer):
  320. new string[] { "Find Differences", "Compare two similar images with few minor differences (fuzz 12)", null,
  321. Path.Combine("Resources", "Images", "newfoundland.jpg"), Path.Combine("Resources", "ImageCompare", "newfoundland-mod.jpg"), "12" },
  322. new string[] { "Invisible Text", "Compare an image with same image that has a semi-transparent text overlay (fuzz 0)", null,
  323. Path.Combine("Resources", "ImageCompare", "seville.png"), Path.Combine("Resources", "ImageCompare", "seville-text.png"), "0" },
  324. new string[] { "PNG vs JPEG", "Compare a PNG image with the same image saved as a 75% quality JPEG (fuzz 6)", null,
  325. Path.Combine("Resources", "ImageCompare", "toronto-lights.png"), Path.Combine("Resources", "ImageCompare", "toronto-lights-75.jpg"), "6" },
  326. new string[] { "Font Hinting", "Compare text rendered with TrueType font hinting on and off (fuzz 1)", null,
  327. Path.Combine("Resources", "ImageCompare", "TrueTypeHinting-on.png"), Path.Combine("Resources", "ImageCompare", "TrueTypeHinting-off.png"), "1" },
  328. };
  329. }
  330.  
  331. // An XYZ color type, based on color math from
  332. // https://www.easyrgb.com/en/math.php
  333. public class ColorXYZ : IEquatable<ColorXYZ>
  334. {
  335. private readonly double _x, _y, _z;
  336.  
  337. // D65 CIE 1964 ref values (sRGB, AdobeRGB):
  338. const double ReferenceX = 94.811;
  339. const double ReferenceY = 100.000;
  340. const double ReferenceZ = 107.304;
  341.  
  342. public double X => _x;
  343. public double Y => _y;
  344. public double Z => _z;
  345.  
  346. private ColorXYZ(double x, double y, double z)
  347. {
  348. _x = x;
  349. _y = y;
  350. _z = z;
  351. }
  352.  
  353. public bool Equals(ColorXYZ other)
  354. {
  355. if ((object)other == null)
  356. return false;
  357. return _x == other._x && _y == other._y && _z == other._z;
  358.  
  359. /*
  360. return Same(_x, other._x) && Same(_y, other._y) && Same(_z, other._z);
  361. bool Same(double a, double b)
  362. {
  363. return Math.Abs(a - b) <= a / 10000;
  364. }
  365. */
  366. }
  367.  
  368. public override bool Equals(object obj)
  369. {
  370. return Equals(obj as ColorXYZ);
  371. }
  372.  
  373. public static bool operator ==(ColorXYZ obj1, ColorXYZ obj2)
  374. {
  375. return (object)obj1 != null && obj1.Equals(obj2);
  376. }
  377.  
  378. public static bool operator !=(ColorXYZ obj1, ColorXYZ obj2)
  379. {
  380. return (object)obj1 == null || !obj1.Equals(obj2);
  381. }
  382.  
  383. public override int GetHashCode()
  384. {
  385. return _x.GetHashCode() ^ _y.GetHashCode() ^ _z.GetHashCode();
  386. }
  387.  
  388. public static ColorXYZ FromXYZ(double x, double y, double z)
  389. {
  390. return new ColorXYZ(x, y, z);
  391. }
  392.  
  393. public static ColorXYZ FromRGB(Color rgb)
  394. {
  395. return FromRGB(rgb.R, rgb.G, rgb.B);
  396. }
  397.  
  398. public static ColorXYZ FromRGB(int rgb)
  399. {
  400. return FromRGB((rgb & 0x00FF0000) >> 16, (rgb & 0x0000FF00) >> 8, rgb & 0x000000FF);
  401. }
  402.  
  403. public static ColorXYZ FromRGB(int r, int g, int b)
  404. {
  405. double var_R = (r / 255d);
  406. double var_G = (g / 255d);
  407. double var_B = (b / 255d);
  408.  
  409. if (var_R > 0.04045)
  410. var_R = Math.Pow((var_R + 0.055) / 1.055, 2.4);
  411. else
  412. var_R = var_R / 12.92;
  413. if (var_G > 0.04045)
  414. var_G = Math.Pow((var_G + 0.055) / 1.055, 2.4);
  415. else
  416. var_G /= 12.92;
  417. if (var_B > 0.04045)
  418. var_B = Math.Pow((var_B + 0.055) / 1.055, 2.4);
  419. else
  420. var_B /= 12.92;
  421.  
  422. var_R *= 100;
  423. var_G *= 100;
  424. var_B *= 100;
  425.  
  426. return new ColorXYZ(
  427. var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805,
  428. var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722,
  429. var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
  430. );
  431. }
  432.  
  433. public static ColorXYZ FromCIELab((double L, double a, double b) lab)
  434. {
  435. return FromCIELab(lab.L, lab.a, lab.b);
  436. }
  437.  
  438. public static ColorXYZ FromCIELab(double L, double a, double b)
  439. {
  440. double var_Y = (L + 16) / 116d;
  441. double var_X = a / 500d + var_Y;
  442. double var_Z = var_Y - b / 200d;
  443.  
  444. if (Math.Pow(var_Y, 3) > 0.008856)
  445. var_Y = Math.Pow(var_Y, 3);
  446. else
  447. var_Y = (var_Y - 16d / 116d) / 7.787;
  448. if (Math.Pow(var_X, 3) > 0.008856)
  449. var_X = Math.Pow(var_X, 3);
  450. else
  451. var_X = (var_X - 16d / 116d) / 7.787;
  452. if (Math.Pow(var_Z, 3) > 0.008856)
  453. var_Z = Math.Pow(var_Z, 3);
  454. else
  455. var_Z = (var_Z - 16d / 116d) / 7.787;
  456.  
  457. return new ColorXYZ(
  458. var_X * ReferenceX,
  459. var_Y * ReferenceY,
  460. var_Z * ReferenceZ
  461. );
  462. }
  463.  
  464. public Color ToRGB()
  465. {
  466. double var_X = _x / 100;
  467. double var_Y = _y / 100;
  468. double var_Z = _z / 100;
  469.  
  470. double var_R = var_X * 3.2406 + var_Y * -1.5372 + var_Z * -0.4986;
  471. double var_G = var_X * -0.9689 + var_Y * 1.8758 + var_Z * 0.0415;
  472. double var_B = var_X * 0.0557 + var_Y * -0.2040 + var_Z * 1.0570;
  473.  
  474. if (var_R > 0.0031308)
  475. var_R = 1.055 * Math.Pow(var_R, (1 / 2.4)) - 0.055;
  476. else
  477. var_R = 12.92 * var_R;
  478. if (var_G > 0.0031308)
  479. var_G = 1.055 * Math.Pow(var_G, (1 / 2.4)) - 0.055;
  480. else
  481. var_G = 12.92 * var_G;
  482. if (var_B > 0.0031308)
  483. var_B = 1.055 * Math.Pow(var_B, (1 / 2.4)) - 0.055;
  484. else
  485. var_B = 12.92 * var_B;
  486.  
  487. return Color.FromArgb(
  488. (int)Math.Round(var_R * 255),
  489. (int)Math.Round(var_G * 255),
  490. (int)Math.Round(var_B * 255)
  491. );
  492. }
  493.  
  494. public (double L, double a, double b) ToCIELab()
  495. {
  496. double var_X = _x / ReferenceX;
  497. double var_Y = _y / ReferenceY;
  498. double var_Z = _z / ReferenceZ;
  499.  
  500. if (var_X > 0.008856)
  501. var_X = Math.Pow(var_X, 1 / 3d);
  502. else
  503. var_X = (7.787 * var_X) + (16d / 116d);
  504. if (var_Y > 0.008856)
  505. var_Y = Math.Pow(var_Y, 1 / 3d);
  506. else
  507. var_Y = (7.787 * var_Y) + (16d / 116d);
  508. if (var_Z > 0.008856)
  509. var_Z = Math.Pow(var_Z, 1 / 3d);
  510. else
  511. var_Z = (7.787 * var_Z) + (16d / 116d);
  512.  
  513. return (
  514. (116 * var_Y) - 16,
  515. 500 * (var_X - var_Y),
  516. 200 * (var_Y - var_Z)
  517. );
  518. }
  519. }
  520. }
  521. }
  522.