views:

366

answers:

4

I have a ~20000 jpg images, some of which are duplicates. Unfortunately, some files have been been tagged with EXIF metadata, so a simple file hash cannot identify the duplicated one.

I am attempting to create a Powershell script to process these, but can find no way to extract only the bitmap data.

The system.drawing.bitmap can only return a bitmap object, not bytes. There's a GetHash() function, but it apparently acts on the whole file.

How can I hash these files in a way that the EXIF information is excluded? I'd prefer to avoid external dependencies if possible.

+4  A: 

You can load the JPEG into a System.Drawing.Image and use it's GetHashCode method

using (var image = Image.FromFile("a.jpg"))
    return image.GetHashCode();

To get the bytes you can

using (var image = Image.FromFile("a.jpg"))
using (var output = new MemoryStream())
{
    image.Save(output, ImageFormat.Bmp);
    return output.ToArray();
}
Jader Dias
Your first approach doesn't work. It returns different hashcodes for the same image (different metadata). The second approach works and is pretty much what everybody else is doing to varying levels of completeness in PowerShell script. :-)
Keith Hill
A: 

Translating to powershell, I get this -

[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
$provider = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider

foreach ($location in $args)
{
    $files=get-childitem $location | where{$_.Extension -match "jpg|jpeg"}
    foreach ($f in $files)
        {
        $bitmap = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $f.FullName
        $stream = New-Object -TypeName System.IO.MemoryStream
        $bitmap.Save($stream)

        $hashbytes = $provider.ComputeHash($stream.ToArray())
        $hashstring = ""
        foreach ($byte in $hashbytes) 
            {$hashstring += $byte.tostring("x2")}  
        $f.FullName
        $hashstring
        echo ""
        }
} 

This produces the same hash regardless of the input file, so something is still not quite correct.

JSacksteder
+4  A: 

This is a PowerShell V2.0 advanced function implemention. It is a bit long but I have verified it gives the same hashcode (generated from the bitmap pixels) on the same picture but with different metadata and file sizes. This is a pipeline capable version that also accepts wildcards and literal paths:

function Get-BitmapHashCode
{
    [CmdletBinding(DefaultParameterSetName="Path")]
    param(
        [Parameter(Mandatory=$true, 
                   Position=0, 
                   ParameterSetName="Path", 
                   ValueFromPipeline=$true, 
                   ValueFromPipelineByPropertyName=$true,
                   HelpMessage="Path to bitmap file")]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Path,

        [Alias("PSPath")]
        [Parameter(Mandatory=$true, 
                   Position=0, 
                   ParameterSetName="LiteralPath", 
                   ValueFromPipelineByPropertyName=$true,
                   HelpMessage="Path to bitmap file")]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $LiteralPath
    )

    Begin {
        Add-Type -AssemblyName System.Drawing
        $sha = new-object System.Security.Cryptography.SHA256Managed
    }

    Process {
        if ($psCmdlet.ParameterSetName -eq "Path")
        {
            # In -Path case we may need to resolve a wildcarded path
            $resolvedPaths = @($Path | Resolve-Path | Convert-Path)
        }
        else 
        {
            # Must be -LiteralPath
            $resolvedPaths = @($LiteralPath | Convert-Path)
        }

        # Find PInvoke info for each specified path       
        foreach ($rpath in $resolvedPaths) 
        {           
            Write-Verbose "Processing $rpath"
            try {
                $bmp    = new-object System.Drawing.Bitmap $rpath
                $stream = new-object System.IO.MemoryStream
                $writer = new-object System.IO.BinaryWriter $stream
                for ($w = 0; $w -lt $bmp.Width; $w++) {
                    for ($h = 0; $h -lt $bmp.Height; $h++) {
                        $pixel = $bmp.GetPixel($w,$h)
                        $writer.Write($pixel.ToArgb())
                    }
                }
                $writer.Flush()
                [void]$stream.Seek(0,'Begin')
                $hash = $sha.ComputeHash($stream)
                [BitConverter]::ToString($hash) -replace '-',''
            }
            finally {
                if ($bmp)    { $bmp.Dispose() }
                if ($writer) { $writer.Close() }
            }
        }  
    }
}
Keith Hill
A: 

Here's a powershell script that produces an SHA256 hash on only the bytes of the image as extracted using LockBits. This should produce a unique hash for each file that is different. Please note, that I didn't include the file iterating code, however it should be a relatively simple task to replace the currently hardcode c:\test.bmp with a foreach directory iterator. The variable $final contains the hex - ascii string of the final hash.

[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[System.Reflection.Assembly]::LoadWithPartialName("System.Drawing.Imaging")
[System.Reflection.Assembly]::LoadWithPartialName("System.Security")


$bmp = [System.Drawing.Bitmap]::FromFile("c:\\test.bmp")
$rect = [System.Drawing.Rectangle]::FromLTRB(0, 0, $bmp.width, $bmp.height)
$lockmode = [System.Drawing.Imaging.ImageLockMode]::ReadOnly               
$bmpData = $bmp.LockBits($rect, $lockmode, $bmp.PixelFormat);
$dataPointer = $bmpData.Scan0;
$totalBytes = $bmpData.Stride * $bmp.Height;
$values = New-Object byte[] $totalBytes
[System.Runtime.InteropServices.Marshal]::Copy($dataPointer, $values, 0, $totalBytes);                
$bmp.UnlockBits($bmpData);

$sha = new-object System.Security.Cryptography.SHA256Managed
$hash = $sha.ComputeHash($values);
$final = [System.BitConverter]::ToString($hash).Replace("-", "");

Perhaps the equivalent C# code will also aid you in understanding:

private static String ImageDataHash(FileInfo imgFile)
{
    using (Bitmap bmp = (Bitmap)Bitmap.FromFile(imgFile.FullName))
    {                
        BitmapData bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp.PixelFormat);
        IntPtr dataPointer = bmpData.Scan0;
        int totalBytes = bmpData.Stride * bmp.Height;
        byte[] values = new byte[totalBytes];                
        System.Runtime.InteropServices.Marshal.Copy(dataPointer, values, 0, totalBytes);                
        bmp.UnlockBits(bmpData);
        SHA256 sha = new SHA256Managed();
        byte[] hash = sha.ComputeHash(values);
        return BitConverter.ToString(hash).Replace("-", "");                
    }
}
BitConverter.ToString() - nice!
Keith Hill