ImageCompare.vb
'''' This code is part of Document Solutions for Imaging demos.'' Copyright (c) MESCIUS inc. All rights reserved.''Imports System.IOImports System.DrawingImports System.Collections.GenericImports System.LinqImports GrapeCity.Documents.DrawingImports GrapeCity.Documents.TextImports GrapeCity.Documents.ImagingImports GCTEXT = GrapeCity.Documents.Text
'' 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 Function GenerateImageStream( ByVal targetMime As String, ByVal pixelSize As Size, ByVal dpi As Single, ByVal opaque As Boolean, Optional sampleParams As String() = Nothing) As Stream
If sampleParams Is Nothing Then sampleParams = GetSampleParamsList()(0) End If
Dim path1 = sampleParams(3) Dim path2 = sampleParams(4) '' Comparison fuzziness (from 0 to MaxDelta): Dim fuzz As Integer If sampleParams Is Nothing Then fuzz = 12 Else fuzz = Integer.Parse(sampleParams(5)) End If '' Different pixels will be this color: Dim highlight = Color.Magenta.ToArgb() '' Fill for empty areas not occupied by any image: Dim fill = 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): Dim Q = (255D / (fuzz + 1)) '' Reference image transparency: Const alphaBack = 55 << 24
Dim bmp1 = New GcBitmap(path1) Dim bmp2 = New GcBitmap(path2)
'' If images are too large, resize to fit target width/height: Dim z1 = 1, z2 = 1 If bmp1.PixelWidth > pixelSize.Width Then z1 = pixelSize.Width / bmp1.PixelWidth End If If bmp1.PixelHeight > pixelSize.Height Then z1 = Math.Min(z1, pixelSize.Height / bmp1.PixelHeight) End If If (bmp2.PixelWidth > pixelSize.Width) Then z2 = pixelSize.Width / bmp2.PixelWidth End If If (bmp2.PixelHeight > pixelSize.Height) Then z2 = Math.Min(z2, pixelSize.Height / bmp2.PixelHeight) End If
'' Resize images to same (minimum) widths: If (bmp1.PixelWidth * z1 > bmp2.PixelWidth * z2) Then z1 = bmp2.PixelWidth / bmp1.PixelWidth ElseIf (bmp2.PixelWidth * z2 > bmp1.PixelWidth * z1) Then z2 = bmp1.PixelWidth / bmp2.PixelWidth End If
If z1 < 1 Then Dim t = bmp1.Resize(Math.Round(bmp1.PixelWidth * z1), Math.Round(bmp1.PixelHeight * z1), If(z1 < 0.5, InterpolationMode.Downscale, InterpolationMode.Cubic)) bmp1.Dispose() bmp1 = t End If If z2 < 1 Then Dim t = bmp2.Resize(Math.Round(bmp2.PixelWidth * z2), Math.Round(bmp2.PixelHeight * z2), If(z2 < 0.5, InterpolationMode.Downscale, InterpolationMode.Cubic)) bmp2.Dispose() bmp2 = t End If
'' 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 Dim MaxDelta = LabDistance((0, -128, -128), (100, 127, 127))
Using bmp = New GcBitmap(pixelSize.Width, pixelSize.Height, False, dpi, dpi) Using g = bmp.CreateGraphics(Color.FromArgb(fill)) '' Total number of different pixels found: Dim differences = 0
Dim w = bmp1.PixelWidth Dim h = Math.Min(bmp1.PixelHeight, bmp2.PixelHeight)
For i = 0 To w - 1 For j = 0 To h - 1 Dim px1 As UInteger = bmp1(i, j) Dim px2 As UInteger = bmp2(i, j)
'' Convert RGB to CIE L*a*b* space to calc the difference: Dim lab1 = ColorXYZ.FromRGB(px1).ToCIELab() If px1 = px2 Then '' 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: Dim gray = Math.Round((lab1.L * 255D) / 100D) bmp(i, j) = alphaBack Or (gray << 16) Or (gray << 8) Or gray Else '' Otherwise, calc the distance between the two colors: Dim lab2 = ColorXYZ.FromRGB(px2).ToCIELab() Dim delta = LabDistance((lab1.L, lab1.a, lab1.b), (lab2.L, lab2.a, lab2.b)) If delta > fuzz Then Dim alpha = Math.Min(255, ((255 * delta) / MaxDelta) * Q) bmp(i, j) = (alpha << 24) Or highlight differences += 1 Else '' See equal pixels comment above: Dim gray = Math.Round((lab1.L * 255D) / 100D) bmp(i, j) = alphaBack Or (gray << 16) Or (gray << 8) Or gray End If End If Next Next
'' For consistency, convert the difference to opaque: bmp.ConvertToOpaque(Color.White)
'' Text layout for info texts: Dim 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: Dim ms = New MemoryStream() If targetMime = MimeTypes.TIFF Then '' 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 tw = New GcTiffWriter(ms) tw.AppendFrame(bmp) bmp1.ConvertToOpaque(Color.White) Using g1 = bmp1.CreateGraphics() tl.Clear() tl.MaxWidth = g1.Width tl.DefaultFormat.BackColor = Color.LightYellow tl.Append(Path.GetFileName(path1)) g1.DrawTextLayout(tl, PointF.Empty) End Using tw.AppendFrame(bmp1) bmp2.ConvertToOpaque(Color.White) Using g2 = bmp2.CreateGraphics() tl.Clear() tl.MaxWidth = g2.Width tl.Append(Path.GetFileName(path2)) g2.DrawTextLayout(tl, PointF.Empty) End Using tw.AppendFrame(bmp2) End Using bmp.SaveAsTiff(ms) Else '' For other formats, tile the diff and sources: Using tbmp = TileImages(pixelSize, bmp, New Size(w, h), bmp1, bmp2, dpi, tl, Path.GetFileName(path1), Path.GetFileName(path2)) Using 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) End Using Select Case targetMime Case MimeTypes.JPEG tbmp.SaveAsJpeg(ms) Case MimeTypes.PNG tbmp.SaveAsPng(ms) Case MimeTypes.BMP tbmp.SaveAsBmp(ms) Case MimeTypes.GIF tbmp.SaveAsGif(ms) Case Else Throw New Exception($"Encoding {targetMime} is not supported.") End Select End Using End If bmp1.Dispose() bmp2.Dispose()
ms.Seek(0, SeekOrigin.Begin) Return ms End Using End Using End Function
Private Shared Function TileImages(ByVal targetSize As Size, ByVal diff As GcBitmap, ByVal diffSize As Size, ByVal bmp1 As GcBitmap, ByVal bmp2 As GcBitmap, ByVal dpi As Single, ByVal tl As TextLayout, ByVal name1 As String, ByVal name2 As String) As GcBitmap
Dim tSize = New Size(targetSize.Width / 2 - 1, targetSize.Width / 2 - 1)
Dim bmp = New GcBitmap(targetSize.Width, targetSize.Height, True, dpi, dpi) Using diffClip = diff.Clip(New Rectangle(Point.Empty, diffSize)) bmp1.ConvertToOpaque(Color.White) bmp2.ConvertToOpaque(Color.White)
Dim ts0 = FitSize(diffClip, tSize) Dim ts1 = FitSize(bmp1, tSize) Dim ts2 = FitSize(bmp2, tSize) Using 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) End Using
Dim x As Integer If (ts0.IsEmpty) Then x = ts1.Width - diffClip.PixelWidth / 2 bmp.BitBlt(diffClip, x, 0) Else x = ts1.Width - ts0.Width / 2 Using tbmp = diffClip.Resize(ts0.Width, ts0.Height, InterpolationMode.Cubic) bmp.BitBlt(tbmp, x, 0) End Using End If If (ts1.IsEmpty) Then bmp.BitBlt(bmp1, 0, ts0.Height + 1) Else Using tbmp = bmp1.Resize(ts1.Width, ts1.Height, InterpolationMode.Cubic) bmp.BitBlt(tbmp, 0, ts0.Height + 1) End Using End If If (ts2.IsEmpty) Then bmp.BitBlt(bmp2, ts1.Width + 1, ts0.Height + 1) Else Using tbmp = bmp2.Resize(ts2.Width, ts2.Height, InterpolationMode.Cubic) bmp.BitBlt(tbmp, ts1.Width + 1, ts0.Height + 1) End Using End If
Using 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)) End Using End Using Return bmp End Function
Private Shared Function FitSize(ByVal bmp As GcBitmap, ByVal size As Size) As Size Dim z As Double = 1 If bmp.PixelWidth > size.Width Then z = size.Width / bmp.PixelWidth End If If bmp.PixelHeight > size.Height Then z = Math.Min(z, size.Height / bmp.PixelHeight) End If If z < 1 Then Return New Size(Math.Round(bmp.PixelWidth * z), Math.Round(bmp.PixelHeight * z)) Else Return Size.Empty '' indicates that no resizing is needed End If End Function
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 Dim dL = lab1.L - lab2.L Dim da = lab1.a - lab2.a Dim db = lab1.b - lab2.b Return Math.Sqrt(dL * dL + da * da + db * db) End Function
Public ReadOnly Property DefaultMime() As String Get Return MimeTypes.TIFF End Get End Property
Public Shared Function GetSampleParamsList() As List(Of 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): Return New List(Of String()) From { New String() {"Find Differences", "Compare two similar images with few minor differences (fuzz 12)", Nothing, 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)", Nothing, 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)", Nothing, 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)", Nothing, Path.Combine("Resources", "ImageCompare", "TrueTypeHinting-on.png"), Path.Combine("Resources", "ImageCompare", "TrueTypeHinting-off.png"), "1"} } End Function
'' An XYZ color type, based on color math from '' https:''www.easyrgb.com/en/math.php Public Class ColorXYZ Implements IEquatable(Of ColorXYZ)
Private ReadOnly _x As Double, _y As Double, _z As Double
'' D65 CIE 1964 ref values (sRGB, AdobeRGB): Const ReferenceX = 94.811 Const ReferenceY = 100.0 Const ReferenceZ = 107.304
Public ReadOnly Property X As Double Get Return _x End Get End Property Public ReadOnly Property Y As Double Get Return _y End Get End Property Public ReadOnly Property Z As Double Get Return _z End Get End Property
Private Sub New(x As Double, y As Double, z As Double) _x = x _y = y _z = z End Sub
Public Overloads Function Equals(other As ColorXYZ) As Boolean Implements IEquatable(Of ColorXYZ).Equals If CType(other, Object) Is Nothing Then Return False End If Return _x = other._x AndAlso _y = other._y AndAlso _z = other._z End Function
Public Overrides Function Equals(obj As Object) As Boolean Return Equals(CType(obj, ColorXYZ)) End Function
Public Shared Operator =(obj1 As ColorXYZ, obj2 As ColorXYZ) As Boolean Return CType(obj1, Object) IsNot Nothing AndAlso obj1.Equals(obj2) End Operator
Public Shared Operator <>(obj1 As ColorXYZ, obj2 As ColorXYZ) As Boolean Return obj1 Is Nothing OrElse Not obj1.Equals(obj2) End Operator
Public Overrides Function GetHashCode() As Integer Return _x.GetHashCode() Xor _y.GetHashCode() Xor _z.GetHashCode() End Function
Public Shared Function FromXYZ(x As Double, y As Double, z As Double) As ColorXYZ Return New ColorXYZ(x, y, z) End Function
Public Shared Function FromRGB(rgb As Color) As ColorXYZ Return FromRGB(rgb.R, rgb.G, rgb.B) End Function
Public Shared Function FromRGB(rgb As UInteger) As ColorXYZ Return FromRGB((rgb And &HFF0000UI) >> 16, (rgb And &HFF00UI) >> 8, rgb And &HFFUI) End Function
Public Shared Function FromRGB(r As Integer, g As Integer, b As Integer) As ColorXYZ Dim var_R = (r / 255D) Dim var_G = (g / 255D) Dim var_B = (b / 255D)
If var_R > 0.04045 Then var_R = Math.Pow((var_R + 0.055) / 1.055, 2.4) Else var_R = var_R / 12.92 End If If var_G > 0.04045 Then var_G = Math.Pow((var_G + 0.055) / 1.055, 2.4) Else var_G /= 12.92 End If If var_B > 0.04045 Then var_B = Math.Pow((var_B + 0.055) / 1.055, 2.4) Else var_B /= 12.92 End If
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 ) End Function
Public Shared Function FromCIELab(lab As (L As Double, a As Double, b As Double)) As ColorXYZ Return FromCIELab(lab.L, lab.a, lab.b) End Function
Public Shared Function FromCIELab(L As Double, a As Double, b As Double) As ColorXYZ Dim var_Y = (L + 16) / 116D Dim var_X = a / 500D + var_Y Dim var_Z = var_Y - b / 200D
If Math.Pow(var_Y, 3) > 0.008856 Then var_Y = Math.Pow(var_Y, 3) Else var_Y = (var_Y - 16D / 116D) / 7.787 End If If Math.Pow(var_X, 3) > 0.008856 Then var_X = Math.Pow(var_X, 3) Else var_X = (var_X - 16D / 116D) / 7.787 End If If Math.Pow(var_Z, 3) > 0.008856 Then var_Z = Math.Pow(var_Z, 3) Else var_Z = (var_Z - 16D / 116D) / 7.787 End If
Return New ColorXYZ( var_X * ReferenceX, var_Y * ReferenceY, var_Z * ReferenceZ ) End Function
Public Function ToRGB() As Color Dim var_X = _x / 100 Dim var_Y = _y / 100 Dim var_Z = _z / 100
Dim var_R = var_X * 3.2406 + var_Y * -1.5372 + var_Z * -0.4986 Dim var_G = var_X * -0.9689 + var_Y * 1.8758 + var_Z * 0.0415 Dim var_B = var_X * 0.0557 + var_Y * -0.204 + var_Z * 1.057
If var_R > 0.0031308 Then var_R = 1.055 * Math.Pow(var_R, (1 / 2.4)) - 0.055 Else var_R = 12.92 * var_R End If If var_G > 0.0031308 Then var_G = 1.055 * Math.Pow(var_G, (1 / 2.4)) - 0.055 Else var_G = 12.92 * var_G End If If var_B > 0.0031308 Then var_B = 1.055 * Math.Pow(var_B, (1 / 2.4)) - 0.055 Else var_B = 12.92 * var_B End If
Return Color.FromArgb( Math.Round(var_R * 255), Math.Round(var_G * 255), Math.Round(var_B * 255) ) End Function
Public Function ToCIELab() As (L As Double, a As Double, b As Double) Dim var_X = _x / ReferenceX Dim var_Y = _y / ReferenceY Dim var_Z = _z / ReferenceZ
If (var_X > 0.008856) Then var_X = Math.Pow(var_X, 1 / 3D) Else var_X = (7.787 * var_X) + (16D / 116D) End If If (var_Y > 0.008856) Then var_Y = Math.Pow(var_Y, 1 / 3D) Else var_Y = (7.787 * var_Y) + (16D / 116D) End If If (var_Z > 0.008856) Then var_Z = Math.Pow(var_Z, 1 / 3D) Else var_Z = (7.787 * var_Z) + (16D / 116D) End If
Return ( (116 * var_Y) - 16, 500 * (var_X - var_Y), 200 * (var_Y - var_Z) ) End Function End ClassEnd Class