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