From 4bd5209110511624c42832fc42705501793a812f Mon Sep 17 00:00:00 2001 From: Muhamad Ibnu Fadhil Date: Fri, 14 Nov 2025 13:52:43 +0700 Subject: [PATCH] first commit --- Compare.Test.csproj | 17 +++++ Program.cs | 173 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 Compare.Test.csproj create mode 100644 Program.cs diff --git a/Compare.Test.csproj b/Compare.Test.csproj new file mode 100644 index 0000000..a7e2e71 --- /dev/null +++ b/Compare.Test.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..f4193c2 --- /dev/null +++ b/Program.cs @@ -0,0 +1,173 @@ +namespace Compare.Test; + +using CoenM.ImageHash; +using CoenM.ImageHash.HashAlgorithms; +using Serilog; + +public static class Program +{ + public static void Main() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .WriteTo.File( + "logs/log-.txt", + rollingInterval: RollingInterval.Day, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .CreateLogger(); + + try + { + Log.Information("Application starting up..."); + RunImageHashing(); + Log.Information("Application has finished successfully."); + } + catch (Exception ex) + { + Log.Fatal(ex, "An unhandled exception occurred, terminating the application."); + } + finally + { + Log.CloseAndFlush(); + } + } + + private static void RunImageHashing() + { + // --- Configuration folders --- + // Takes the folder directories of each contestant + // Each image should be .PNG and have "XX_" prefix e.g : 01_Image.png, 02_Image2.png, etc + string[] inputDirectories = + [ + "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/renjayarz", + "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/reinardras", + "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/meizar", + "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/olymrifki", + "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/fikribahru", + "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/benscode", + "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/dennisarfan" + ]; + + string outputCsvPath = "hash_analysis_results.csv"; + IImageHash hashAlgorithm = new PerceptualHash(); + + Log.Information("Generating hashes using {AlgorithmName}...", hashAlgorithm.GetType().Name); + + var groupedImages = new Dictionary>(); + + // Group images by prefix + foreach (string dir in inputDirectories) + { + if (!Directory.Exists(dir)) + { + Log.Warning("Directory not found: {DirectoryPath}", dir); + continue; + } + + foreach (string file in Directory.GetFiles(dir, "*.png")) + { + string prefix = Path.GetFileName(file).Split('_')[0]; + if (!groupedImages.ContainsKey(prefix)) + groupedImages[prefix] = new List(); + + groupedImages[prefix].Add(file); + } + } + + var csvLines = new List { "TestPrefix,FileName,ImageHash,SimilarityToRepresentative(%)" }; + + foreach (var group in groupedImages.OrderBy(g => g.Key)) + { + string prefix = group.Key; + var imagePaths = group.Value; + + if (imagePaths.Count < 2) + { + Log.Warning("Skipping {Prefix}: Not enough images to compare ({ImageCount}).", prefix, imagePaths.Count); + continue; + } + + Log.Information("=== Processing group {Prefix} ({ImageCount} images) ===", prefix, imagePaths.Count); + + // Calculate hashes + var hashResults = new Dictionary(); + foreach (var path in imagePaths) + { + try + { + using var stream = File.OpenRead(path); + ulong hash = hashAlgorithm.Hash(stream); + hashResults[path] = hash; + string relativePath = $"{Path.GetFileName(Path.GetDirectoryName(path))}/{Path.GetFileName(path)}"; + + Log.Debug("Hashed {ImagePath}: {ImageHash}", relativePath, hash); + } + catch (Exception ex) + { + Log.Error(ex, "Error hashing {ImagePath}", path); + } + } + + if (hashResults.Count < 2) + { + Log.Warning("Skipping {Prefix}: Not enough valid images after hashing ({HashedCount}).", prefix, hashResults.Count); + continue; + } + + // --- Compute Representative Hash --- + ulong representativeHash = ComputeRepresentativeHash(hashResults.Values.ToList()); + Log.Information("-> Representative Hash for {Prefix}: {RepresentativeHash}", prefix, representativeHash); + Log.Information("-----------------------------------------------------"); + + // --- Compute similarity to representative hash --- + foreach (var entry in hashResults) + { + double similarity = CompareHash.Similarity(entry.Value, representativeHash); + string fileName = $"{Path.GetFileName(Path.GetDirectoryName(entry.Key))}/{Path.GetFileName(entry.Key)}"; + csvLines.Add($"{prefix},{fileName},{entry.Value},{similarity:F6}"); + Log.Information("{FileName,-70} | Similarity: {Similarity:F2}%", fileName, similarity); + } + Log.Information("-----------------------------------------------------\n"); + } + + // --- Write CSV output --- + try + { + File.WriteAllLines(outputCsvPath, csvLines); + Log.Information("Results successfully saved to: {CsvPath}", Path.GetFullPath(outputCsvPath)); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to write CSV file to {CsvPath}", outputCsvPath); + } + } + + // === Compute the bitwise representative hash (majority vote using consensus) === + private static ulong ComputeRepresentativeHash(List hashes) + { + int[] bitCounts = new int[64]; + + // Count how many 1s are in each bit + foreach (ulong hash in hashes) + { + for (int bit = 0; bit < 64; bit++) + { + if (((hash >> bit) & 1UL) == 1UL) + bitCounts[bit]++; + } + } + + ulong result = 0UL; + int threshold = hashes.Count / 2; + + // If more than half of the hashes have a 1 in a bit, set the corresponding bit in the result, otherwise 0 + for (int bit = 0; bit < 64; bit++) + { + if (bitCounts[bit] > threshold) + result |= 1UL << bit; + } + + return result; + } +} \ No newline at end of file