''
'' This code is part of Document Solutions for Imaging demos.
'' Copyright (c) MESCIUS inc. All rights reserved.
''
Imports System.IO
Imports System.Drawing
Imports System.Collections.Generic
Imports System.Linq
Imports GrapeCity.Documents.Drawing
Imports GrapeCity.Documents.Text
Imports GrapeCity.Documents.Imaging
Imports GCTEXT = GrapeCity.Documents.Text
Imports GCDRAW = GrapeCity.Documents.Drawing
'' 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 = New TextLayout(g.Resolution) With {.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:
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 Class
End Class