Last active
March 14, 2024 16:15
-
-
Save joshooaj/db1f8d983484174726b42322fefc75a7 to your computer and use it in GitHub Desktop.
Compare images using the dHash algorithm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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