Skip to content

Commit b90dbca

Browse files
Zixu_WangZixu_Wang
authored andcommitted
Implement image similarity detection.
1 parent dedf68c commit b90dbca

21 files changed

+3538
-1
lines changed

Images/1.png

176 KB
Loading

Images/2.png

329 KB
Loading

Images/3.png

406 KB
Loading

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,31 @@
11
# ImageSimilarityDetection-UI
2-
Find similar images in the same directory by calculating the hash.
2+
Find similar images in the directory by aHash/dHash/pHash.
3+
4+
## Supported formats
5+
.jpg; .jpeg; .png
6+
7+
## Purpose
8+
Reduce duplication in an image folder.
9+
10+
## Principle
11+
1. Implement aHash, dHash, pHash to generate the fingerprint of images.
12+
2. Calculate the hamming distance between every two images.
13+
3. Export image pairs with high similarity(hamming distance).
14+
15+
## Usage
16+
1. Get `SimilarImages.exe` in the following ways.
17+
- Build this project in Visual Studio.
18+
- Find it in [_Output](_Output).
19+
- Find it in [Releases](https://github.com/Roy0309/ImageSimilarityDetection-UI/releases).
20+
21+
2. Double click `SimilarImages.exe`. Select an image folder and set arguments (the default is suggested arguments). Enjoy!
22+
23+
## Output
24+
1. Compare two identical images.
25+
<img width="400" src="Images/1.png"/>
26+
27+
2. Compare two images with different resolution.
28+
<img width="400" src="Images/2.png"/>
29+
30+
3. Compare two images with similar content.
31+
<img width="400" src="Images/3.png"/>

SimilarImages/SimilarImages.sln

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.30104.148
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimilarImages", "SimilarImages\SimilarImages.csproj", "{CD6DE35F-14AB-42A3-8303-71A579BCFAD1}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{CD6DE35F-14AB-42A3-8303-71A579BCFAD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{CD6DE35F-14AB-42A3-8303-71A579BCFAD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{CD6DE35F-14AB-42A3-8303-71A579BCFAD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{CD6DE35F-14AB-42A3-8303-71A579BCFAD1}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {C5EDAF3C-F86C-4908-AD23-2A93D8E114B1}
24+
EndGlobalSection
25+
EndGlobal
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<configuration>
3+
<startup>
4+
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
5+
</startup>
6+
</configuration>

SimilarImages/SimilarImages/Form1.Designer.cs

Lines changed: 506 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
using Microsoft.VisualBasic.FileIO;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.ComponentModel;
5+
using System.Diagnostics;
6+
using System.Drawing;
7+
using System.Drawing.Drawing2D;
8+
using System.IO;
9+
using System.Linq;
10+
using System.Windows.Forms;
11+
12+
namespace SimilarImages
13+
{
14+
public partial class Form1 : Form
15+
{
16+
private string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
17+
private int precision = 20;
18+
private double threshold = 0.8;
19+
private ImageHash.HashEnum hashEnum = ImageHash.HashEnum.Difference;
20+
private InterpolationMode interpolationMode = InterpolationMode.Default;
21+
private List<Tuple<string, string, double>> tuples = null;
22+
23+
public Form1()
24+
{
25+
InitializeComponent();
26+
}
27+
28+
#region Config
29+
30+
private void Form1_Load(object sender, EventArgs e)
31+
{
32+
cmb_Algorithm.SelectedIndex = 0;
33+
cmb_Interpolation.SelectedIndex = 0;
34+
tb_Directory.Text = folderPath;
35+
}
36+
37+
private void btn_Directory_Click(object sender, EventArgs e)
38+
{
39+
FolderBrowserDialog fbd = new FolderBrowserDialog
40+
{
41+
Description = "Choose a folder to find similar images.",
42+
ShowNewFolderButton = false
43+
};
44+
fbd.ShowDialog();
45+
if (string.IsNullOrEmpty(fbd.SelectedPath)) { return; }
46+
tb_Directory.Text = fbd.SelectedPath;
47+
folderPath = fbd.SelectedPath;
48+
}
49+
50+
private void tb_Precision_KeyPress(object sender, KeyPressEventArgs e)
51+
{
52+
// Alow 0-9 and backspace
53+
if ((e.KeyChar < '0' || e.KeyChar > '9') && e.KeyChar != '\b')
54+
{ e.Handled = true; }
55+
}
56+
57+
private void tb_Threshold_KeyPress(object sender, KeyPressEventArgs e)
58+
{
59+
// Allow 0-9, backspace and '.'
60+
if ((e.KeyChar < '0' || e.KeyChar > '9') &&
61+
e.KeyChar != '\b' && e.KeyChar != '.')
62+
{ e.Handled = true; }
63+
// Only one '.'
64+
if (tb_Threshold.Text.Contains('.') && e.KeyChar == '.') { e.Handled = true; }
65+
// '.' can only come after '0'
66+
if (tb_Threshold.Text == "0" && e.KeyChar != '.') { e.Handled = true; }
67+
}
68+
69+
private void cmb_Algorithm_SelectedIndexChanged(object sender, EventArgs e)
70+
{
71+
hashEnum = (ImageHash.HashEnum)cmb_Algorithm.SelectedIndex;
72+
}
73+
74+
private void cmb_Interpolation_SelectedIndexChanged(object sender, EventArgs e)
75+
{
76+
switch (cmb_Interpolation.SelectedIndex)
77+
{
78+
case 0: interpolationMode = InterpolationMode.Default; break;
79+
case 1: interpolationMode = InterpolationMode.NearestNeighbor; break;
80+
case 2: interpolationMode = InterpolationMode.HighQualityBilinear; break;
81+
case 3: interpolationMode = InterpolationMode.HighQualityBicubic; break;
82+
default: break;
83+
}
84+
}
85+
86+
#endregion Config
87+
88+
#region Process
89+
90+
private void btn_Process_Click(object sender, EventArgs e)
91+
{
92+
bool validPrecision = int.TryParse(tb_Precision.Text, out precision);
93+
bool validThreshold = double.TryParse(tb_Threshold.Text, out threshold);
94+
bool validFolderPath = !string.IsNullOrEmpty(tb_Directory.Text);
95+
96+
if (!AssertConfig(validPrecision, "Please input valid precision.")) { return; }
97+
if (!AssertConfig(precision >= 8, "Precision should be greater than 8.")) { return; }
98+
if (!AssertConfig(validThreshold,"Please input valid threshold [0,1).")) { return; }
99+
if (!AssertConfig(validFolderPath, "Please input valid folder path.")) { return; }
100+
101+
progressBar1.Visible = true;
102+
lb_Empty.Visible = true;
103+
btn_Process.Enabled = false;
104+
bgw_Calculate.RunWorkerAsync();
105+
}
106+
107+
private bool AssertConfig(bool successCondition, string failureTip)
108+
{
109+
if (!successCondition)
110+
{
111+
MessageBox.Show(failureTip, "Notice",
112+
MessageBoxButtons.OK, MessageBoxIcon.Information);
113+
}
114+
return successCondition;
115+
}
116+
117+
private void bgw_Calculate_DoWork(object sender, DoWorkEventArgs e)
118+
{
119+
Stopwatch watch = new Stopwatch();
120+
watch.Start();
121+
122+
tuples = ImageHash.GetSimilarity(folderPath, out int count,
123+
precision, interpolationMode, hashEnum, threshold);
124+
lb_Count.Invoke((Action)(() => { lb_Count.Text = count.ToString(); }));
125+
126+
watch.Stop();
127+
Debug.WriteLine("ElapsedTime: " + watch.ElapsedMilliseconds + " ms");
128+
}
129+
130+
private void bgw_Calculate_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
131+
{
132+
lvw_Result.Items.Clear();
133+
progressBar1.Visible = false;
134+
btn_Process.Enabled = true;
135+
if (tuples != null) { lb_Empty.Visible = false; }
136+
else { lb_Empty.Visible = true; return; }
137+
138+
// Generate result list
139+
lvw_Result.BeginUpdate();
140+
for (int i = 0; i < tuples.Count; i++)
141+
{
142+
lvw_Result.Items.Add($"Result {i + 1} - {tuples[i].Item3:P1}");
143+
}
144+
lvw_Result.EndUpdate();
145+
lvw_Result.Items[0].Selected = true;
146+
lvw_Result.Select();
147+
}
148+
149+
#endregion Process
150+
151+
#region Comparison
152+
153+
private void lvw_Result_SelectedIndexChanged(object sender, EventArgs e)
154+
{
155+
if (lvw_Result.SelectedItems.Count < 1) { return; }
156+
157+
// Dispose previous images
158+
pictureBox1.Image?.Dispose();
159+
pictureBox2.Image?.Dispose();
160+
161+
// Show images
162+
var selectedTuple = tuples[lvw_Result.SelectedIndices[0]];
163+
try
164+
{
165+
pictureBox1.Image = new Bitmap(Path.Combine(folderPath, selectedTuple.Item1));
166+
lb_Image1.Text = selectedTuple.Item1;
167+
lb_Resolution1.Text = $"{pictureBox1.Image.Width}*{pictureBox1.Image.Height}";
168+
}
169+
catch (ArgumentException)
170+
{
171+
pictureBox1.Image = null;
172+
lb_Image1.Text = "Deleted";
173+
lb_Resolution1.Text = "";
174+
}
175+
try
176+
{
177+
pictureBox2.Image = new Bitmap(Path.Combine(folderPath, selectedTuple.Item2));
178+
lb_Image2.Text = selectedTuple.Item2;
179+
lb_Resolution2.Text = $"{pictureBox2.Image.Width}*{pictureBox2.Image.Height}";
180+
}
181+
catch (ArgumentException)
182+
{
183+
pictureBox2.Image = null;
184+
lb_Image2.Text = "Deleted";
185+
lb_Resolution2.Text = "";
186+
}
187+
}
188+
189+
private void btn_Delete1_Click(object sender, EventArgs e)
190+
{
191+
DeleteImage(pictureBox1, lb_Image1);
192+
}
193+
194+
private void btn_Delete2_Click(object sender, EventArgs e)
195+
{
196+
DeleteImage(pictureBox2, lb_Image2);
197+
}
198+
199+
private void DeleteImage(PictureBox pictureBox, Label label)
200+
{
201+
if (pictureBox.Image == null) { return; }
202+
203+
DialogResult dr = MessageBox.Show($"Move this image [{label.Text}] to recycle bin?",
204+
"Warning", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning);
205+
if (dr == DialogResult.OK)
206+
{
207+
pictureBox.Image.Dispose();
208+
pictureBox.Image = null;
209+
FileSystem.DeleteFile(Path.Combine(folderPath, label.Text),
210+
UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
211+
label.Text = "Deleted";
212+
}
213+
}
214+
215+
private void btn_Open1_Click(object sender, EventArgs e)
216+
{
217+
if (pictureBox1.Image == null) { return; }
218+
Process.Start(Path.Combine(folderPath, lb_Image1.Text));
219+
}
220+
221+
private void btn_Open2_Click(object sender, EventArgs e)
222+
{
223+
if (pictureBox2.Image == null) { return; }
224+
Process.Start(Path.Combine(folderPath, lb_Image2.Text));
225+
}
226+
227+
#endregion Comparison
228+
}
229+
}

0 commit comments

Comments
 (0)