Skip to content

Commit

Permalink
Implement image similarity detection.
Browse files Browse the repository at this point in the history
  • Loading branch information
Zixu_Wang authored and Zixu_Wang committed Jun 8, 2020
1 parent dedf68c commit b90dbca
Show file tree
Hide file tree
Showing 21 changed files with 3,538 additions and 1 deletion.
Binary file added Images/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Images/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Images/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,31 @@
# ImageSimilarityDetection-UI
Find similar images in the same directory by calculating the hash.
Find similar images in the directory by aHash/dHash/pHash.

## Supported formats
.jpg; .jpeg; .png

## Purpose
Reduce duplication in an image folder.

## Principle
1. Implement aHash, dHash, pHash to generate the fingerprint of images.
2. Calculate the hamming distance between every two images.
3. Export image pairs with high similarity(hamming distance).

## Usage
1. Get `SimilarImages.exe` in the following ways.
- Build this project in Visual Studio.
- Find it in [_Output](_Output).
- Find it in [Releases](https://github.com/Roy0309/ImageSimilarityDetection-UI/releases).

2. Double click `SimilarImages.exe`. Select an image folder and set arguments (the default is suggested arguments). Enjoy!

## Output
1. Compare two identical images.
<img width="400" src="Images/1.png"/>

2. Compare two images with different resolution.
<img width="400" src="Images/2.png"/>

3. Compare two images with similar content.
<img width="400" src="Images/3.png"/>
25 changes: 25 additions & 0 deletions SimilarImages/SimilarImages.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30104.148
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimilarImages", "SimilarImages\SimilarImages.csproj", "{CD6DE35F-14AB-42A3-8303-71A579BCFAD1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CD6DE35F-14AB-42A3-8303-71A579BCFAD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD6DE35F-14AB-42A3-8303-71A579BCFAD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD6DE35F-14AB-42A3-8303-71A579BCFAD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD6DE35F-14AB-42A3-8303-71A579BCFAD1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C5EDAF3C-F86C-4908-AD23-2A93D8E114B1}
EndGlobalSection
EndGlobal
6 changes: 6 additions & 0 deletions SimilarImages/SimilarImages/App.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
506 changes: 506 additions & 0 deletions SimilarImages/SimilarImages/Form1.Designer.cs

Large diffs are not rendered by default.

229 changes: 229 additions & 0 deletions SimilarImages/SimilarImages/Form1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
using Microsoft.VisualBasic.FileIO;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
using System.Windows.Forms;

namespace SimilarImages
{
public partial class Form1 : Form
{
private string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
private int precision = 20;
private double threshold = 0.8;
private ImageHash.HashEnum hashEnum = ImageHash.HashEnum.Difference;
private InterpolationMode interpolationMode = InterpolationMode.Default;
private List<Tuple<string, string, double>> tuples = null;

public Form1()
{
InitializeComponent();
}

#region Config

private void Form1_Load(object sender, EventArgs e)
{
cmb_Algorithm.SelectedIndex = 0;
cmb_Interpolation.SelectedIndex = 0;
tb_Directory.Text = folderPath;
}

private void btn_Directory_Click(object sender, EventArgs e)
{
FolderBrowserDialog fbd = new FolderBrowserDialog
{
Description = "Choose a folder to find similar images.",
ShowNewFolderButton = false
};
fbd.ShowDialog();
if (string.IsNullOrEmpty(fbd.SelectedPath)) { return; }
tb_Directory.Text = fbd.SelectedPath;
folderPath = fbd.SelectedPath;
}

private void tb_Precision_KeyPress(object sender, KeyPressEventArgs e)
{
// Alow 0-9 and backspace
if ((e.KeyChar < '0' || e.KeyChar > '9') && e.KeyChar != '\b')
{ e.Handled = true; }
}

private void tb_Threshold_KeyPress(object sender, KeyPressEventArgs e)
{
// Allow 0-9, backspace and '.'
if ((e.KeyChar < '0' || e.KeyChar > '9') &&
e.KeyChar != '\b' && e.KeyChar != '.')
{ e.Handled = true; }
// Only one '.'
if (tb_Threshold.Text.Contains('.') && e.KeyChar == '.') { e.Handled = true; }
// '.' can only come after '0'
if (tb_Threshold.Text == "0" && e.KeyChar != '.') { e.Handled = true; }
}

private void cmb_Algorithm_SelectedIndexChanged(object sender, EventArgs e)
{
hashEnum = (ImageHash.HashEnum)cmb_Algorithm.SelectedIndex;
}

private void cmb_Interpolation_SelectedIndexChanged(object sender, EventArgs e)
{
switch (cmb_Interpolation.SelectedIndex)
{
case 0: interpolationMode = InterpolationMode.Default; break;
case 1: interpolationMode = InterpolationMode.NearestNeighbor; break;
case 2: interpolationMode = InterpolationMode.HighQualityBilinear; break;
case 3: interpolationMode = InterpolationMode.HighQualityBicubic; break;
default: break;
}
}

#endregion Config

#region Process

private void btn_Process_Click(object sender, EventArgs e)
{
bool validPrecision = int.TryParse(tb_Precision.Text, out precision);
bool validThreshold = double.TryParse(tb_Threshold.Text, out threshold);
bool validFolderPath = !string.IsNullOrEmpty(tb_Directory.Text);

if (!AssertConfig(validPrecision, "Please input valid precision.")) { return; }
if (!AssertConfig(precision >= 8, "Precision should be greater than 8.")) { return; }
if (!AssertConfig(validThreshold,"Please input valid threshold [0,1).")) { return; }
if (!AssertConfig(validFolderPath, "Please input valid folder path.")) { return; }

progressBar1.Visible = true;
lb_Empty.Visible = true;
btn_Process.Enabled = false;
bgw_Calculate.RunWorkerAsync();
}

private bool AssertConfig(bool successCondition, string failureTip)
{
if (!successCondition)
{
MessageBox.Show(failureTip, "Notice",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
return successCondition;
}

private void bgw_Calculate_DoWork(object sender, DoWorkEventArgs e)
{
Stopwatch watch = new Stopwatch();
watch.Start();

tuples = ImageHash.GetSimilarity(folderPath, out int count,
precision, interpolationMode, hashEnum, threshold);
lb_Count.Invoke((Action)(() => { lb_Count.Text = count.ToString(); }));

watch.Stop();
Debug.WriteLine("ElapsedTime: " + watch.ElapsedMilliseconds + " ms");
}

private void bgw_Calculate_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
lvw_Result.Items.Clear();
progressBar1.Visible = false;
btn_Process.Enabled = true;
if (tuples != null) { lb_Empty.Visible = false; }
else { lb_Empty.Visible = true; return; }

// Generate result list
lvw_Result.BeginUpdate();
for (int i = 0; i < tuples.Count; i++)
{
lvw_Result.Items.Add($"Result {i + 1} - {tuples[i].Item3:P1}");
}
lvw_Result.EndUpdate();
lvw_Result.Items[0].Selected = true;
lvw_Result.Select();
}

#endregion Process

#region Comparison

private void lvw_Result_SelectedIndexChanged(object sender, EventArgs e)
{
if (lvw_Result.SelectedItems.Count < 1) { return; }

// Dispose previous images
pictureBox1.Image?.Dispose();
pictureBox2.Image?.Dispose();

// Show images
var selectedTuple = tuples[lvw_Result.SelectedIndices[0]];
try
{
pictureBox1.Image = new Bitmap(Path.Combine(folderPath, selectedTuple.Item1));
lb_Image1.Text = selectedTuple.Item1;
lb_Resolution1.Text = $"{pictureBox1.Image.Width}*{pictureBox1.Image.Height}";
}
catch (ArgumentException)
{
pictureBox1.Image = null;
lb_Image1.Text = "Deleted";
lb_Resolution1.Text = "";
}
try
{
pictureBox2.Image = new Bitmap(Path.Combine(folderPath, selectedTuple.Item2));
lb_Image2.Text = selectedTuple.Item2;
lb_Resolution2.Text = $"{pictureBox2.Image.Width}*{pictureBox2.Image.Height}";
}
catch (ArgumentException)
{
pictureBox2.Image = null;
lb_Image2.Text = "Deleted";
lb_Resolution2.Text = "";
}
}

private void btn_Delete1_Click(object sender, EventArgs e)
{
DeleteImage(pictureBox1, lb_Image1);
}

private void btn_Delete2_Click(object sender, EventArgs e)
{
DeleteImage(pictureBox2, lb_Image2);
}

private void DeleteImage(PictureBox pictureBox, Label label)
{
if (pictureBox.Image == null) { return; }

DialogResult dr = MessageBox.Show($"Move this image [{label.Text}] to recycle bin?",
"Warning", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning);
if (dr == DialogResult.OK)
{
pictureBox.Image.Dispose();
pictureBox.Image = null;
FileSystem.DeleteFile(Path.Combine(folderPath, label.Text),
UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin);
label.Text = "Deleted";
}
}

private void btn_Open1_Click(object sender, EventArgs e)
{
if (pictureBox1.Image == null) { return; }
Process.Start(Path.Combine(folderPath, lb_Image1.Text));
}

private void btn_Open2_Click(object sender, EventArgs e)
{
if (pictureBox2.Image == null) { return; }
Process.Start(Path.Combine(folderPath, lb_Image2.Text));
}

#endregion Comparison
}
}
Loading

0 comments on commit b90dbca

Please sign in to comment.