Skip to content

Commit b5ee94e

Browse files
committed
Cueing up optimization work
1 parent 715fa98 commit b5ee94e

File tree

2 files changed

+250
-0
lines changed

2 files changed

+250
-0
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
namespace CoenM.ImageHash.HashAlgorithms
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
using System.Linq;
7+
using System.Runtime.CompilerServices;
8+
9+
using SixLabors.ImageSharp;
10+
using SixLabors.ImageSharp.PixelFormats;
11+
using SixLabors.ImageSharp.Processing;
12+
13+
/// <summary>
14+
/// Perceptual hash; Calculate a hash of an image by first transforming the image to an 64x64 grayscale bitmap and then using the Discrete cosine transform to remove the high frequencies.
15+
/// </summary>
16+
public class PerceptualHashOptimized : IImageHash
17+
{
18+
private const int Size = 64;
19+
private static readonly double Sqrt2DivSize = Math.Sqrt(2D / Size);
20+
private static readonly double Sqrt2 = 1 / Math.Sqrt(2);
21+
22+
/// <inheritdoc />
23+
public ulong Hash(Image<Rgba32> image)
24+
{
25+
if (image == null)
26+
throw new ArgumentNullException(nameof(image));
27+
28+
var rows = new double[Size][];
29+
var sequence = new double[Size];
30+
var matrix = new double[Size][];
31+
32+
image.Mutate(ctx => ctx
33+
.Resize(Size, Size)
34+
.Grayscale(GrayscaleMode.Bt601)
35+
.AutoOrient());
36+
37+
// Calculate the DCT for each row.
38+
for (var y = 0; y < Size; y++)
39+
{
40+
for (var x = 0; x < Size; x++)
41+
sequence[x] = image[x, y].R;
42+
43+
rows[y] = Dct1D(sequence);
44+
}
45+
46+
// Calculate the DCT for each column.
47+
for (var x = 0; x < Size; x++)
48+
{
49+
for (var y = 0; y < Size; y++)
50+
sequence[y] = rows[y][x];
51+
52+
matrix[x] = Dct1D(sequence);
53+
}
54+
55+
// Only use the top 8x8 values.
56+
var top8X8 = new List<double>(Size);
57+
for (var y = 0; y < 8; y++)
58+
{
59+
for (var x = 0; x < 8; x++)
60+
top8X8.Add(matrix[y][x]);
61+
}
62+
63+
var topRight = top8X8.ToArray();
64+
65+
// Get Median.
66+
var median = CalculateMedian64Values(topRight);
67+
68+
// Calculate hash.
69+
var mask = 1UL << (Size - 1);
70+
var hash = 0UL;
71+
72+
for (var i = 0; i < Size; i++)
73+
{
74+
if (topRight[i] > median)
75+
hash |= mask;
76+
77+
mask = mask >> 1;
78+
}
79+
80+
return hash;
81+
}
82+
83+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
84+
private static double CalculateMedian64Values(IReadOnlyCollection<double> values)
85+
{
86+
Debug.Assert(values.Count == 64, "This DCT method works with 64 doubles.");
87+
return values.OrderBy(value => value).Skip(31).Take(2).Average();
88+
}
89+
90+
/// <summary>
91+
/// One dimensional Discrete Cosine Transformation.
92+
/// </summary>
93+
/// <param name="values">Should be an array of doubles of length 64.</param>
94+
/// <returns>array of doubles of length 64.</returns>
95+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
96+
private static double[] Dct1D(IReadOnlyList<double> values)
97+
{
98+
Debug.Assert(values.Count == 64, "This DCT method works with 64 doubles.");
99+
var coefficients = new double[Size];
100+
101+
for (var coef = 0; coef < Size; coef++)
102+
{
103+
for (var i = 0; i < Size; i++)
104+
coefficients[coef] += values[i] * Math.Cos(((2.0 * i) + 1.0) * coef * Math.PI / (2.0 * Size));
105+
106+
coefficients[coef] *= Sqrt2DivSize;
107+
if (coef == 0)
108+
coefficients[coef] *= Sqrt2;
109+
}
110+
111+
return coefficients;
112+
}
113+
}
114+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
namespace CoenM.ImageHash.Test.Algorithms
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
using CoenM.ImageHash.HashAlgorithms;
8+
using CoenM.ImageHash.Test.Internal;
9+
using FluentAssertions;
10+
using Xunit;
11+
12+
public class PerceptualHashOptimizedTest
13+
{
14+
private readonly PerceptualHashOptimized sut;
15+
16+
private readonly Dictionary<string, ulong> expectedHashes = new Dictionary<string, ulong>
17+
{
18+
{ "Alyson_Hannigan_500x500_0.jpg", 17839858461443178030 },
19+
{ "Alyson_Hannigan_500x500_1.jpg", 17839823311430827566 },
20+
{ "Alyson_Hannigan_200x200_0.jpg", 17839858461443178030 },
21+
{ "Alyson_Hannigan_4x4_0.jpg", 17409736169497899465 },
22+
{ "github_1.jpg", 13719320793338945348 },
23+
{ "github_2.jpg", 13783795072850083657 },
24+
};
25+
26+
public PerceptualHashOptimizedTest()
27+
{
28+
sut = new PerceptualHashOptimized();
29+
}
30+
31+
[Theory]
32+
[InlineData("Alyson_Hannigan_500x500_0.jpg", 17839823311430827566)]
33+
[InlineData("Alyson_Hannigan_500x500_1.jpg", 17839823311430827566)]
34+
[InlineData("Alyson_Hannigan_200x200_0.jpg", 17839823311430827566)]
35+
[InlineData("Alyson_Hannigan_4x4_0.jpg", 17409736169531453642)]
36+
[InlineData("github_1.jpg", 13719320793338945348)]
37+
[InlineData("github_2.jpg", 13783795072850083657)]
38+
public void HashImagesTest(string filename, ulong expectedHash)
39+
{
40+
// arrange
41+
ulong result;
42+
43+
// act
44+
using (var stream = TestHelper.OpenStream(filename))
45+
result = sut.Hash(stream);
46+
47+
// assert
48+
result.Should().Be(expectedHash);
49+
}
50+
51+
[Fact]
52+
[SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "Manually reviewed")]
53+
public void NotAnImageShouldThrowExceptionTest()
54+
{
55+
// arrange
56+
const string filename = "Not_an_image.txt";
57+
58+
// act
59+
using (var stream = TestHelper.OpenStream(filename))
60+
{
61+
Action act = () => sut.Hash(stream);
62+
63+
// assert
64+
act.Should().Throw<SixLabors.ImageSharp.UnknownImageFormatException>();
65+
}
66+
}
67+
68+
[Fact]
69+
public void NullArgumentShouldThrowArgumentNullExceptionTest()
70+
{
71+
// arrange
72+
73+
// act
74+
Action act = () => sut.Hash(null);
75+
76+
// assert
77+
act.Should().Throw<ArgumentNullException>();
78+
}
79+
80+
[Fact]
81+
public void ImageWithFilterShouldHaveAlmostOrExactly100Similarity1Test()
82+
{
83+
// arrange
84+
var hash1 = expectedHashes["Alyson_Hannigan_500x500_0.jpg"];
85+
var hash2 = expectedHashes["Alyson_Hannigan_500x500_1.jpg"];
86+
87+
// act
88+
var result = CompareHash.Similarity(hash1, hash2);
89+
90+
// assert
91+
result.Should().Be(96.875);
92+
}
93+
94+
[Fact]
95+
public void ResizedImageShouldHaveAlmostOrExactly100Similarity2Test()
96+
{
97+
// arrange
98+
var hash1 = expectedHashes["Alyson_Hannigan_500x500_0.jpg"];
99+
var hash2 = expectedHashes["Alyson_Hannigan_200x200_0.jpg"];
100+
101+
// act
102+
var result = CompareHash.Similarity(hash1, hash2);
103+
104+
// assert
105+
result.Should().Be(100);
106+
}
107+
108+
[Fact]
109+
public void ComparingExtreamlySmallImageShouldDecreaseSimilarityTest()
110+
{
111+
// arrange
112+
var hash1 = expectedHashes["Alyson_Hannigan_4x4_0.jpg"];
113+
var hash2 = expectedHashes["Alyson_Hannigan_500x500_0.jpg"];
114+
115+
// act
116+
var result = CompareHash.Similarity(hash1, hash2);
117+
118+
// assert
119+
result.Should().Be(59.375);
120+
}
121+
122+
[Fact]
123+
public void TwoDifferentImagesOfGithubArePrettySimilarTests()
124+
{
125+
// arrange
126+
var hash1 = expectedHashes["github_1.jpg"];
127+
var hash2 = expectedHashes["github_2.jpg"];
128+
129+
// act
130+
var result = CompareHash.Similarity(hash1, hash2);
131+
132+
// assert
133+
result.Should().Be(71.875);
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)