Skip to content

Instantly share code, notes, and snippets.

@joshooaj
Last active March 14, 2024 16:15
Show Gist options
  • Save joshooaj/db1f8d983484174726b42322fefc75a7 to your computer and use it in GitHub Desktop.
Save joshooaj/db1f8d983484174726b42322fefc75a7 to your computer and use it in GitHub Desktop.
Compare images using the dHash algorithm
function ConvertFrom-HexString {
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline)]
[string]
$InputObject
)
process {
$bytes = [byte[]]::new($InputObject.Length / 2)
for ($index = 0; $index -lt $bytes.Length; $index++) {
$bytes[$index] = [convert]::ToByte($InputObject.SubString($index * 2, 2), 16)
}
Write-Output $bytes -NoEnumerate
}
}
function ConvertTo-HexString {
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0)]
[byte[]]
$InputObject
)
process {
[string]::Join('', ($InputObject | ForEach-Object { $_.ToString('x2') }))
}
}
function ConvertTo-DHashImage {
<#
.SYNOPSIS
Returns a grayscale 9x8 resolution image based on the input image.
.DESCRIPTION
The `ConvertTo-DHashImage` cmdlet returns a grayscale 9x8 resolution image
based on the input image.
.PARAMETER Image
Specifies the input image.
.PARAMETER ColorMatrix
Optionally specifies the RGB values to use in the ColorMatrix used for grayscale conversion.
.EXAMPLE
[System.Drawing.Image]::FromFile('C:\path\to\image.jpg') | ConvertTo-DHashImage
Create a new System.Drawing.Image object from image.jpg, and produce a grayscale 9x8 representation of it.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline, Position = 1)]
[System.Drawing.Image]
$Image,
[Parameter()]
[float[]]
$ColorMatrix = @(0.299, 0.587, 0.114)
)
process {
$r = $ColorMatrix[0]
$g = $ColorMatrix[1]
$b = $ColorMatrix[2]
$grayScale = [float[][]]@(
[float[]]@($r, $r, $r, 0, 0),
[float[]]@($g, $g, $g, 0, 0),
[float[]]@($b, $b, $b, 0, 0),
[float[]]@( 0, 0, 0, 1, 0),
[float[]]@( 0, 0, 0, 0, 1)
)
try {
$dst = [drawing.bitmap]::new(9, 8)
$dstRectangle = [drawing.rectangle]::new(0, 0, $dst.Width, $dst.Height)
$graphics = [drawing.graphics]::FromImage($dst)
$graphics.CompositingMode = [drawing.drawing2d.compositingmode]::SourceOver
$graphics.CompositingQuality = [drawing.drawing2d.CompositingQuality]::HighQuality
$graphics.InterpolationMode = [drawing.drawing2d.InterpolationMode]::HighQualityBicubic
$graphics.PixelOffsetMode = [drawing.drawing2d.PixelOffsetMode]::None
$imgAttr = [drawing.imaging.imageattributes]::new()
$imgAttr.SetWrapMode([drawing.drawing2d.wrapmode]::Clamp)
$imgAttr.SetColorMatrix([drawing.imaging.colormatrix]::new($grayScale))
$graphics.DrawImage($Image, $dstRectangle, 0, 0, $Image.Width, $Image.Height, [drawing.graphicsunit]::Pixel, $imgAttr)
$dst
} finally {
$imgAttr, $graphics | Where-Object { $null -ne $_ } | ForEach-Object {
$_.Dispose()
}
}
}
}
function Get-DHash {
<#
.SYNOPSIS
Computes the dHash value for the provided image.
.DESCRIPTION
The `Get-DHash` cmdlet computes the dHash value for the provided image. The dHash is a 64-bit representation of the
image, returned as a hexadecimal string. The dHash values for two images can be compared using Compare-DHash, and
the resulting value represents the number of bits that are different between the two images, or the
"Hamming distance".
The dHash is computed using the following algorithm. See the blog post referenced in the notes for more information.
1. Convert the image to grayscale.
2. Resize the image to 9x8.
3. For each of the 8 rows in the resulting image, check if each pixel is brighter than the neighbor to the right. If
it is, that bit is set to 1.
4. Convert the 8 resulting bytes to a hexadecimal string.
.PARAMETER Path
Specifies the path to an image file.
.PARAMETER Bytes
Specifies an array of bytes representing an image.
.PARAMETER OutFile
For diagnostic purposes, you may provide a path to save the resized, grayscale representation of the provided image created for dHash calculation.
.PARAMETER ColorMatrix
Optionally you may provide a custom ColorMatrix used to create a grayscale representation of the source image.
.EXAMPLE
$dhash1 = Get-DHash ./image1.jpg
$dhash2 = Get-DHash ./image2.jpg
Compare-DHash $dhash1 $dhash2
Computes the dHash values for two different images, and then compares the
dHash values. The result is the number of bits that do not match between the
two difference-hashes.
.NOTES
The inspiration for the dHash concept and these functions comes from a blog
post by Dr. Neal Krawetz on [The Hacker Factor Blog](https://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html).
#>
[CmdletBinding(DefaultParameterSetName = 'FromFile')]
[OutputType([string])]
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'FromFile')]
[string]
$Path,
[Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName, ParameterSetName = 'FromBytes')]
[Alias('Content')]
[byte[]]
$Bytes,
# Saves a copy of the grayscale, resized reference image used for calculating dHash for diagnostic purposes.
[Parameter()]
[string]
$OutFile,
[Parameter()]
[float[]]
$ColorMatrix = @(0.299, 0.587, 0.114)
)
begin {
Add-Type -AssemblyName System.Drawing
}
process {
try {
$dHash = [byte[]]::new(8)
if ($PSCmdlet.ParameterSetName -eq 'FromFile') {
$Path = (Resolve-Path $Path).Path
$Bytes = [io.file]::ReadAllBytes($Path)
}
$ms = [io.memorystream]::new()
$ms.Write($Bytes, 0, $Bytes.Length)
$ms.Flush()
$ms.Position = 0
$src = [drawing.image]::FromStream($ms)
$dst = ConvertTo-DHashImage -Image $src
for ($y = 0; $y -lt $dst.Height; $y++) {
$byte = [byte]0
for ($x = 0; $x -lt ($dst.Width - 1); $x++) {
$thisPixel = $dst.GetPixel($x, $y).GetBrightness()
$nextPixel = $dst.GetPixel($x + 1, $y).GetBrightness()
$thisPixelIsBrighter = [byte]($thisPixel -gt $nextPixel)
$byte = $byte -shl 1
$byte = $byte -bor $thisPixelIsBrighter
}
$dHash[$y] = $byte
}
ConvertTo-HexString -InputObject $dHash
if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('OutFile')) {
$OutFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutFile)
$dst.Save($OutFile)
}
} finally {
$src, $dst | Where-Object { $null -ne $_ } | ForEach-Object {
$_.Dispose()
}
}
}
}
function Compare-DHash {
<#
.SYNOPSIS
Compares the provided dHash strings and returns the difference as an integer between 0 and 64.
.DESCRIPTION
The `Compare-DHash` cmdlet compares the provided dHash strings and returns the difference as an
integer between 0 and 64.
.PARAMETER DHash1
Specifies a case-insensitive dHash string with 16 hexadecimal characters.
.PARAMETER DHash2
Specifies a case-insensitive dHash string with 16 hexadecimal characters.
.EXAMPLE
$dhash1 = Get-DHash ./image1.jpg
$dhash2 = Get-DHash ./image2.jpg
Compare-DHash $dhash1 $dhash2
Computes the dHash values for two different images, and then compares the
dHash values. The result is the number of bits that do not match between the
two difference-hashes.
.NOTES
The inspiration for the dHash concept and these functions comes from a blog
post by Dr. Neal Krawetz on [The Hacker Factor Blog](https://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html).
#>
[CmdletBinding()]
[OutputType([int])]
param (
[Parameter(Mandatory, Position = 0)]
[string]
$DHash1,
[Parameter(Mandatory, Position = 1)]
[string]
$DHash2
)
process {
$difference = 0;
for ($index = 0; $index -lt 8; $index++) {
$byte1 = [convert]::ToByte($DHash1.SubString($index * 2, 2), 16)
$byte2 = [convert]::ToByte($DHash2.SubString($index * 2, 2), 16)
$xor = $byte1 -bxor $byte2
for ($bit = 8; $bit -gt 0; $bit--) {
$difference += $xor -band 1
$xor = $xor -shr 1
}
}
$difference
}
}
function Get-VmsCameraDHash {
<#
.SYNOPSIS
Calculates a dHash based on multiple samples of live video.
.DESCRIPTION
The `Get-VmsCameraDHash` cmdlet calculates a dHash based on multiple samples of live video. Each image sample is
reduced to a dHash, then a median value of each bit from each of the collected dHash samples is extracted into a
new dHash.
.PARAMETER Camera
Specifies the camera to calculate a dHash value from.
.PARAMETER Samples
Specifies the number of image samples to use for dHash calculation.
.PARAMETER Interval
Specifies the amount of time to wait between each live video sample.
.EXAMPLE
$camera = Select-Camera
$hash1 = $camera | Get-VmsCameraDHash -Samples 30 -Interval ([timespan]::FromMilliseconds(500))
$hash2 = $camera | Get-VmsCameraDHash -Samples 30 -Interval ([timespan]::FromMilliseconds(500))
Compare-DHash $hash1 $hash2
Generates two dHash values based on 30 live image samples taken at least 500ms apart, then returns a number
representing how many of the 64 bits in the two dHashs are different. A value of 10 or less is a strong indicator
that the two dHash values are based on similar images.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[VideoOS.Platform.ConfigurationItems.Camera]
$Camera,
[Parameter()]
[int]
$Samples = 30,
[Parameter()]
[timespan]
$Interval = ([timespan]::FromMilliseconds(100))
)
process {
try {
$progress = @{
Activity = 'Calculating dHash from video'
Status = 'Collecting image samples'
PercentComplete = 0
Completed = $false
}
Write-Progress @progress
$hashes = [byte[][]]::new($Samples)
for ($sampleNum = 0; $sampleNum -lt $Samples; $sampleNum++) {
do {
$snapshot = $Camera | Get-Snapshot -Live
Start-Sleep -Milliseconds $Interval.TotalMilliseconds
} while ($snapshot.Content.Count -eq 0)
$hashes[$sampleNum] = $snapshot | Get-DHash | ConvertFrom-HexString
$progress.PercentComplete = ($sampleNum + 1) / $Samples * 100
Write-Progress @progress
}
$progress.Status = 'Processing image samples'
Write-Progress @progress
$avg = [byte[]]::new(8)
for ($row = 0; $row -lt 8; $row++) {
$byte = [byte]0
for ($bit = 0; $bit -lt 8; $bit++) {
$byte = $byte -shl 1
$temp = [byte[]]::new($Samples)
for ($sampleNum = 0; $sampleNum -lt $hashes.Count; $sampleNum++) {
$temp[$sampleNum] = ($hashes[$sampleNum][$row] -shr (7 - $bit)) -band 1
}
[array]::Sort($temp)
if ($Samples % 2) {
$byte = $byte -bor $temp[[math]::Floor($Samples / 2)]
} else {
$byte = $byte -bor [math]::Round(($temp[$Samples / 2] + $temp[$Samples / 2 - 1]) / 2)
}
}
$avg[$row] = $byte
}
$progress.Completed = $true
Write-Progress @progress
ConvertTo-HexString -InputObject $avg
} finally {
if (-not $progress.Completed) {
$progress.Completed = $true
Write-Progress @progress
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment